Your .swp Files Are Telling on You: A Git Forensics Guide

Valentin Lobstein /
Table of Contents

TL;DR

Text editors like Vim and nano create swap files (.swp, .swn, .swo) to protect against crashes. People accidentally commit them with git add . or git add -A. They delete the file, push again, and think the problem is solved. It’s not. The blob lives in git history forever. And that blob contains your username, hostname, file paths, and potentially the full content of whatever you were editing - passwords, tokens, config files, everything.

This post shows how to find them, extract them, and what you can learn from them. Bonus: it also tells you if someone is lying about using Vim. 😉


What Are Swap Files?

When you open a file in Vim, it creates a swap file in the same directory. If you’re editing config.yml, Vim creates .config.yml.swp. If that swap file already exists, it creates .config.yml.swo, then .config.yml.swn, and so on. These files serve as crash recovery - if your terminal dies mid-edit, Vim can restore your unsaved changes from the swap file.

Nano does the same thing. It creates swap files with a different internal format but the same purpose.

The key difference is in the magic bytes at the start of the file:

EditorMagic BytesASCII
Vim62 30 56 49 4db0VIM
nano62 30 6e 61 6e 6fb0nano

These magic bytes are how you can immediately tell which editor created a swap file, even if the filename doesn’t give it away.

What’s Inside a Swap File?

This is where it gets interesting. Swap files aren’t just raw copies of the file content. They contain metadata:

  • Username of whoever was editing the file
  • Hostname of their machine
  • Full file path on disk
  • Process ID of the editor
  • Timestamps
  • The actual content of the file being edited (or at least the parts that were modified)

That’s a lot of information for a file most people don’t even know exists.


How They End Up in Git

It’s always the same story:

# Someone edits a file
vim .env

# They save and quit, but the swap file persists
# (maybe they had two sessions open, maybe Vim crashed)

# Then they commit everything
git add .
git commit -m "update config"
git push

That git add . grabs everything - including .config.yml.swp, .env.swp, or whatever swap files are sitting in the directory.

Sometimes people notice. They delete the swap file, commit the deletion, and move on. But the damage is already done. Git stores every version of every file that was ever committed. The blob containing that swap file is still there, sitting in the repository history, waiting to be extracted.


Why Deleting Doesn’t Help

Git is a content-addressable filesystem. Every file you commit gets stored as a blob, identified by its SHA-1 hash. When you delete a file and commit, Git records the deletion but the original blob stays in the object store. It’s still there in the commit where it was added.

Commit A: adds .env.swp        <- blob exists here
Commit B: deletes .env.swp     <- deletion recorded, but blob from A still exists

Anyone who can clone the repo can walk the history and extract that blob. The file is “deleted” in the current tree but very much alive in the history.


Finding Swap Files in Git History

Let’s say you’re auditing a repository - your own or one you have access to. Here’s how to find swap files that were committed and later deleted.

Step 1: Find the Deletion Commit

git log --diff-filter=D --summary -- "*.swp"

This shows all commits that deleted files matching *.swp. You’ll get output like:

commit 7a3f2c1...
Author: someone <someone@example.com>
Date:   Mon Apr 14 12:00:00 2026 +0200

    remove swap file

 delete mode 100644 .env.swp

You can also search for nano swap files and other patterns:

git log --diff-filter=D --summary -- "*.swp" "*.swo" "*.swn"

Step 2: Get the Parent Commit

The deletion commit removed the file. You need the commit before the deletion - the one where the file still existed. That’s the parent commit.

# If the deletion commit is 7a3f2c1
git show 7a3f2c1^:.env.swp > recovered.swp

The ^ means “parent of this commit”, and :<path> extracts the file content at that point in history. That’s it - you now have the swap file.

If you’re working with the GitHub API instead of a local clone:

# Get the parent commit SHA
gh api repos/OWNER/REPO/commits/7a3f2c1 --jq '.parents[0].sha'

# Then get the file from the parent tree
gh api repos/OWNER/REPO/contents/.env.swp?ref=PARENT_SHA --jq '.content' | base64 -d > recovered.swp

Step 3: Examine the Swap File

Now inspect it:

xxd recovered.swp | head -20

You’ll see something like this for a Vim swap file:

00000000: 6230 5649 4d20 382e 3200 0000 0000 0000  b0VIM 8.2.......
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
...
000001f0: 6a6f 686e 0000 0000 0000 0000 0000 0000  john............
00000200: 0000 0000 0000 0000 0000 0000 0000 0000  ................
...
00000210: 6465 762d 7365 7276 6572 0000 0000 0000  dev-server......
...
00000230: 2f68 6f6d 652f 6a6f 686e 2f70 726f 6a65  /home/john/proje
00000240: 6374 2f2e 656e 7600 0000 0000 0000 0000  ct/.env.........

Right there: username john, hostname dev-server, file path /home/john/project/.env. And if the file had content when the swap was created, that content is in there too.

For a nano swap file, the structure is different but just as revealing. Here’s a real-world example found in a public git history:

00000000: 6230 6e61 6e6f 2038 2e37 2e31 0000 0000  b0nano 8.7.1....
00000010: 0000 0000 0000 0000 e14e 0000 726f 6f74  .........N..root
00000040: 0000 0000 6b61 6c69 0000 0000 0000 0000  ....kali........
00000060: 0000 0000 0000 0000 0000 0000 7368 656c  ............shel
00000070: 6c5f 7374 6162 0000 0000 0000 0000 0000  l_stab..........
000003e0: 0000 0000 0000 0000 0000 0000 0000 0055  ...............U

According to nano’s source code (write_lockfile function), the binary layout is:

OffsetSizeFieldValue in this dump
0-12 bytesMagic bytes (0x62 0x30 = “b0”)b0
2-1211 bytesEditor + versionnano 8.7.1
24-274 bytesPID (little-endian)0x4ee1 = PID 20193
28-4316 bytesUsernameroot
68-9932 bytesHostnamekali
108-875768 bytesFile path being editedshell_stab
10071 byteModified flag (0x55 = yes)0x55 = file was modified

From one binary blob you know: the editor and version, the OS username, the machine hostname, the file being edited, the PID of the session, and whether the file had unsaved changes. The commit message said “Remove Vim swap file.” The binary says nano.


Bonus: Catching Editor Lies

Both nano and vim use .swp files. So if someone commits a swap file and writes “Remove Vim swap file” in the commit message, but the first bytes say b0nano - congratulations, you just caught someone lying about their editor.

In this industry, we verify everything. Including your text editor. The hex don’t lie. 😉


Creating a Demo Swap File

Let’s create a fake nano swap file to demonstrate. No need for an actual editor session - we can build one with Python:

import struct

# nano swap file header
magic = b"b0nano 8.1\x00"
padding = b"\x00" * (128 - len(magic))

# Fake metadata
username = b"admin\x00" + b"\x00" * 26
hostname = b"prod-web-01\x00" + b"\x00" * 20
filepath = b"/var/www/app/.env\x00" + b"\x00" * 46

# Fake file content that was being edited
content = b"DB_PASSWORD=SuperSecret123!\nAWS_SECRET_KEY=AKIA...\nAPI_TOKEN=ghp_xxxxxxxxxxxx\n"

with open(".env.swp", "wb") as f:
    f.write(magic)
    f.write(padding)
    f.write(username)
    f.write(hostname)
    f.write(filepath)
    f.write(b"\x00" * 64)  # more padding
    f.write(content)

Now inspect it:

xxd .env.swp
00000000: 6230 6e61 6e6f 2038 2e31 0000 0000 0000  b0nano 8.1......
...
00000080: 6164 6d69 6e00 0000 0000 0000 0000 0000  admin...........
...
000000a0: 7072 6f64 2d77 6562 2d30 3100 0000 0000  prod-web-01.....
...
000000c0: 2f76 6172 2f77 7777 2f61 7070 2f2e 656e  /var/www/app/.en
000000d0: 7600 0000 0000 0000 0000 0000 0000 0000  v...............
...
00000120: 4442 5f50 4153 5357 4f52 443d 5375 7065  DB_PASSWORD=Supe
00000130: 7253 6563 7265 7431 3233 210a 4157 535f  rSecret123!.AWS_
00000140: 5345 4352 4554 5f4b 4559 3d41 4b49 412e  SECRET_KEY=AKIA.
00000150: 2e2e 0a41 5049 5f54 4f4b 454e 3d67 6870  ...API_TOKEN=ghp
00000160: 5f78 7878 7878 7878 7878 7878 780a       _xxxxxxxxxxxx.

Username, hostname, file path, and the actual content of what was being edited. All from a swap file.


Searching at Scale

If you’re looking at this from a reconnaissance perspective, you can search across public repositories. GitHub’s search doesn’t index binary files directly, but you can use the API to look for known patterns:

# Search for .swp files in a specific repo's history
git log --all --diff-filter=A --summary -- "*.swp" "*.swo" "*.swn"

The --diff-filter=A flag shows when files were added instead of deleted. This finds the original commit where the swap file was introduced.

For a quick binary content check once you’ve extracted a file:

# Check for Vim swap files
xxd recovered.swp | grep -q "b0VIM" && echo "Vim swap file"

# Check for nano swap files
xxd recovered.swp | grep -q "b0nano" && echo "nano swap file"

# Extract readable strings
strings recovered.swp

The strings command alone can reveal a lot. Usernames, paths, and file contents are all stored as plain text in the swap file’s binary data.


Actually Cleaning It Up

So you found a swap file in your repo’s history. Now what?

Option 1: git filter-branch (old school)

git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch "*.swp"' \
  --prune-empty --tag-name-filter cat -- --all

This rewrites the entire history, removing the file from every commit. This changes all commit hashes from the point where the file was introduced onward. Force-push required.

Option 2: BFG Repo-Cleaner (faster)

java -jar bfg.jar --delete-files '*.swp' repo.git

BFG is significantly faster than git filter-branch and easier to use. It’s the recommended approach for most cases.

Option 3: git filter-repo (modern)

git filter-repo --invert-paths --path-glob '*.swp'

This is the modern replacement for git filter-branch. Faster, safer, and the Git project itself recommends it.

The Catch

Even after rewriting history and force-pushing, GitHub keeps blobs accessible by their SHA hash for a while. If someone grabbed the SHA before you cleaned up, they can still fetch that blob directly:

gh api repos/OWNER/REPO/git/blobs/BLOB_SHA

GitHub eventually garbage-collects unreferenced blobs, but don’t count on it happening quickly. If the swap file contained real secrets, rotate those secrets immediately. Don’t wait for garbage collection.


Prevention

The best fix is never committing them in the first place.

.gitignore

Add this to your .gitignore (or better, your global gitignore at ~/.config/git/ignore):

# Swap files
*.swp
*.swo
*.swn
*~

# Vim
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]

# nano
.*.swp

Global Gitignore

Set it once, forget about it:

git config --global core.excludesfile ~/.config/git/ignore

Then add the patterns above to that file. This protects every repo on your machine without needing a .gitignore in each one.

Pre-commit Hook

If you want a safety net, add a pre-commit hook that rejects swap files:

#!/bin/bash
# .git/hooks/pre-commit

if git diff --cached --name-only | grep -qE '\.(swp|swo|swn)$'; then
    echo "ERROR: Swap file detected in staged files."
    echo "Unstage it before committing."
    git diff --cached --name-only | grep -E '\.(swp|swo|swn)$'
    exit 1
fi

Make it executable with chmod +x .git/hooks/pre-commit.

The Simplest Fix

Run git status before you commit. Read the output. If you see a .swp file in the staging area, unstage it. That’s it. Most of these accidents happen because people blindly run git add . without checking what they’re about to commit.


What Attackers Can Learn

A single swap file can give an attacker:

  • Username - who was editing the file, useful for credential stuffing or SSH brute-force
  • Hostname - internal server names, useful for mapping infrastructure
  • File paths - directory structure, deployment paths, where configs live
  • Editor version - sometimes useful for targeting known vulnerabilities in old editors
  • File content - if the original file contained secrets, those secrets are in the swap file

The metadata alone is valuable for reconnaissance. The file content is the jackpot. Imagine someone was editing .env, config.php, secrets.yml, or id_rsa when the swap file was created. That content is sitting in git history, base64-encoded in GitHub’s API, waiting to be extracted.


Final Thoughts

Swap files are one of those things nobody thinks about until it’s too late. They’re created automatically, they live in your working directory, and git add . doesn’t discriminate. One careless commit and your username, hostname, file paths, and potentially your secrets are permanently recorded in git history.

The fix is simple: set up a global gitignore and stop using git add .. Stage files explicitly. Check git status. It takes three seconds and saves you from having to rewrite your entire git history later.

If you’re doing recon on a target, check the git history for swap files. You might be surprised what you find.