July 19, 2026
OverTheWire Bandit Walkthrough — Level 31 → 32 | 30-Day Cybersecurity Learning Journey (Day 31)
Forcing a file past a .gitignore rule and pushing it to a remote so a server-side hook hands over the next password.

By William | Cybersecurity & SOC Analyst
7 min read
Introduction
Day 31. Bandit Level 31 to Level 32. This level flips the entire git arc on its head. For four levels the task has been pulling secrets out of repositories. This time the repository wants me to put something in. The task is to push a file to the remote and only then does the server reward me with the next credential.
The core skill here is the write side of git, staging, committing and pushing, plus understanding what .gitignore actually does and does not do. The repo sets a small trap with an ignore rule and getting past it teaches a lesson that matters far beyond this single challenge.
By the end of this article you will understand the git add, commit and push workflow, why .gitignore blocked the file and how to force a file through with git add -f. You will also see why treating .gitignore as a security control is a mistake that defenders regularly see exploited in real environments.
Level Objective
The official OverTheWire page points to a git repository at ssh://bandit31-git@localhost/home/bandit31-git/repo on port 2220. The password for the bandit31-git account is the same as the bandit31 password. The objective is to clone the repository and follow its instructions, which this time means creating a specific file and pushing it back to the master branch.
Approach
I cloned the repository from /tmp into a dedicated directory as usual, supplying the bandit31 password when git prompted. Then I read README.md, which laid out the task directly. I needed to create a file named key.txt containing the exact text May I come in? and push it to the master branch:
cd /tmp
mkdir james31git
cd james31git
git clone ssh://bandit31-git@bandit.labs.overthewire.org:2220/home/bandit31-git/repo
cd repocd /tmp
mkdir james31git
cd james31git
git clone ssh://bandit31-git@bandit.labs.overthewire.org:2220/home/bandit31-git/repo
cd repo
I created the file with echo and confirmed its contents with cat. Then I noticed a .gitignore file in the listing and read it. It contained a single rule, *.txt, which tells git to ignore every file ending in .txt. That is precisely the file I was told to add, so a normal git add would silently refuse it:
ls -la
cat README.md
echo 'May I come in?' > key.txt
cat key.txt
cat .gitignorels -la
cat README.md
echo 'May I come in?' > key.txt
cat key.txt
cat .gitignoreI forced the file into the staging area with git add -f, which overrides the ignore rule. Then I committed it with a message and pushed to the master branch. The server ran a validation hook, accepted the file and printed the password for Level 32 directly in the push output:
git add -f key.txt
git commit -m "add key"
git push origin mastergit add -f key.txt
git commit -m "add key"
git push origin master
Commands Used
# Move to a writable directory for the clone
cd /tmp
# Create a dedicated working directory
mkdir james31git
cd james31git
# Clone the repository over SSH
git clone ssh://bandit31-git@bandit.labs.overthewire.org:2220/home/bandit31-git/repo
# Enter the cloned repository
cd repo
# List all files, revealing the .gitignore file
ls -la
# Read the task from the README
cat README.md
# Create the required file with the exact content
echo 'May I come in?' > key.txt
# Confirm the file contents
cat key.txt
# Read the ignore rule blocking .txt files
cat .gitignore
# Force the file into staging despite the ignore rule
git add -f key.txt
# Commit the staged file with a message
git commit -m "add key"
# Push the commit to the master branch on the remote
git push origin master# Move to a writable directory for the clone
cd /tmp
# Create a dedicated working directory
mkdir james31git
cd james31git
# Clone the repository over SSH
git clone ssh://bandit31-git@bandit.labs.overthewire.org:2220/home/bandit31-git/repo
# Enter the cloned repository
cd repo
# List all files, revealing the .gitignore file
ls -la
# Read the task from the README
cat README.md
# Create the required file with the exact content
echo 'May I come in?' > key.txt
# Confirm the file contents
cat key.txt
# Read the ignore rule blocking .txt files
cat .gitignore
# Force the file into staging despite the ignore rule
git add -f key.txt
# Commit the staged file with a message
git commit -m "add key"
# Push the commit to the master branch on the remote
git push origin masterCommand Breakdown
echo 'May I come in?' > key.txt Creates the required file and writes the exact text the README demands into it. The content has to match precisely because the server validates it against the expected value before responding.
cat .gitignore Reads the ignore file. The single rule *.txt matches every file ending in .txt, which means git skips key.txt during a normal add. Reading this file is what explains the trap the level sets.
git add -f key.txt Stages the file for commit. The -f flag forces the add even though .gitignore says to skip it. Without -f, git quietly ignores the file and the resulting push would contain nothing at all.
git commit -m "add key" Records the staged change as a commit with a short message. The commit is local until it is pushed. Nothing has left the local machine yet at this point.
git push origin master Sends the local commit to the remote repository on the master branch. The remote runs a server-side hook that validates the file against the task and on success prints the next password in the response output.
Lesson Learned
The main technical takeaway is the full git write workflow, add then commit then push, and the role .gitignore plays in the middle of it. An ignored file is not staged unless it is forced, so a push can silently miss exactly the file it was meant to send with no obvious error.
What clicked during this level is that .gitignore is a convenience, not a lock. It keeps clutter out of commits by default but it stops nothing that a user actually wants to add. The -f flag walks straight through it. That distinction is easy to overlook until it causes a confusing failed push.
Going forward the habit is to read .gitignore early whenever a push is involved, so it is clear upfront whether the target file is being filtered. Then force it if needed. Recognising the ignore rule before the push saves a round of committing nothing and wondering why.
git add filename— stage a file for the next commitgit add -f filename— force staging of a file that .gitignore would normally skipgit commit -m "message"— record staged changes as a commit with a messagegit push origin branch— send local commits to a remote branchcat .gitignore— see which file patterns are being filtered from staginggit status— check what is staged, unstaged or currently ignored
🔴 SOC Analyst Insight
This level teaches a misconception that causes real breaches. Developers treat .gitignore as if it protects sensitive files, listing secrets and credential files there and assuming they are safe. They are not. .gitignore only prevents accidental default staging. Anyone can force an ignored file into a commit and an ignored file that was committed once before the rule existed stays in the history forever.
During a repository audit, an ignored file should never be assumed to be a protected file. Two things need checking: whether anything sensitive was committed before it was later ignored, and whether ignored files are being force-added into commits regardless of the rule.
# Search the full history across every branch for sensitive file patterns even if currently ignored
git log --all --full-history -- "*.env" "*.key" "*.pem" "credentials*"# Search the full history across every branch for sensitive file patterns even if currently ignored
git log --all --full-history -- "*.env" "*.key" "*.pem" "credentials*"The command above searches the entire history across every branch for sensitive file patterns, including ones currently listed in .gitignore. If a secret file was ever committed, this surfaces it regardless of whether it is ignored now. It is precisely the audit that catches the false sense of security .gitignore creates.
The connection back to this level is direct. Here a .txt file was forced past the ignore rule with a single flag, proving the rule is trivially bypassed. In production the same mechanics mean a .gitignore entry is documentation, not enforcement. Real protection comes from never committing secrets, using pre-commit secret scanners and keeping credentials in a vault or environment variables. Once a secret reaches a remote it is out of your control and must be rotated immediately.
Key Takeaway
The git write workflow is add, commit, push, and .gitignore sits quietly in the middle of it filtering files. But .gitignore is a convenience, not a security boundary. A single -f flag forces an ignored file straight through and anything committed before an ignore rule existed lives on in history regardless. Never rely on .gitignore to protect secrets. Keep them out of the repository entirely.
30-Day Cybersecurity Learning Journey — Progress
🟢 Open Day — Setup & Series Introduction | OverTheWire Bandit
✅ Day 0. — Bandit Level 0 | First Login
✅ Day 1. — Bandit Level 1 → 2 | Special Characters
✅ Day 2. — Bandit Level 2 → 3 | Spaces in Filenames
✅ Day 3. — Bandit Level 3 → 4 | Hidden Files
✅ Day 4. — Bandit Level 4 → 5 | File Types
✅ Day 5. — Bandit Level 5 → 6 | find with Properties
✅ Day 6. — Bandit Level 6 → 7 | find across Filesystem
✅ Day 7. — Bandit Level 7 → 8 | grep
✅ Day 8. — Bandit Level 8 → 9 | sort and uniq
✅ Day 9. — Bandit Level 9 → 10 | strings and grep
✅ Day 10. — Bandit Level 10 → 11 | base64
✅ Day 11. — Bandit Level 11 → 12 | ROT13 and tr
✅ Day 12. — Bandit Level 12 → 13 | hexdump and compression
✅ Day 13. — Bandit Level 13 → 14 | SSH keys
✅ Day 14. — Bandit Level 14 → 15 | Netcat
✅ Day 15. — Bandit Level 15 → 16 | SSL and OpenSSL
✅ Day 16. — Bandit Level 16 → 17 | Port Scanning
✅ Day 17. — Bandit Level 17 → 18 | diff
✅ Day 18. — Bandit Level 18 → 19 | SSH command execution
✅ Day 19. — Bandit Level 19 → 20 | Setuid binaries
✅ Day 20. — Bandit Level 20 → 21 | Network services
✅ Day 21. — Bandit Level 21 → 22 | Cron jobs
✅ Day 22. — Bandit Level 22 → 23 | Cron and bash scripting
✅ Day 23. — Bandit Level 23 → 24 | Writing cron scripts
✅ Day 24. — Bandit Level 24 → 25 | Brute forcing and loops
✅ Day 25. — Bandit Level 25 → 26 | Restricted shells
✅ Day 26. — Bandit Level 26 → 27 | SUID privilege escalation
✅ Day 27. — Bandit Level 27 → 28 | git clone over SSH
✅ Day 28. — Bandit Level 28 → 29 | git log and git show
✅ Day 29. — Bandit Level 29 → 30 | git branches
✅ Day 30. — Bandit Level 30 → 31 | git tags
✅ Day 31. — Bandit Level 31 → 32 | git push and .gitignore ← today
⬜ Day 32. — Bandit Level 32 → 33 | coming next🟢 Open Day — Setup & Series Introduction | OverTheWire Bandit
✅ Day 0. — Bandit Level 0 | First Login
✅ Day 1. — Bandit Level 1 → 2 | Special Characters
✅ Day 2. — Bandit Level 2 → 3 | Spaces in Filenames
✅ Day 3. — Bandit Level 3 → 4 | Hidden Files
✅ Day 4. — Bandit Level 4 → 5 | File Types
✅ Day 5. — Bandit Level 5 → 6 | find with Properties
✅ Day 6. — Bandit Level 6 → 7 | find across Filesystem
✅ Day 7. — Bandit Level 7 → 8 | grep
✅ Day 8. — Bandit Level 8 → 9 | sort and uniq
✅ Day 9. — Bandit Level 9 → 10 | strings and grep
✅ Day 10. — Bandit Level 10 → 11 | base64
✅ Day 11. — Bandit Level 11 → 12 | ROT13 and tr
✅ Day 12. — Bandit Level 12 → 13 | hexdump and compression
✅ Day 13. — Bandit Level 13 → 14 | SSH keys
✅ Day 14. — Bandit Level 14 → 15 | Netcat
✅ Day 15. — Bandit Level 15 → 16 | SSL and OpenSSL
✅ Day 16. — Bandit Level 16 → 17 | Port Scanning
✅ Day 17. — Bandit Level 17 → 18 | diff
✅ Day 18. — Bandit Level 18 → 19 | SSH command execution
✅ Day 19. — Bandit Level 19 → 20 | Setuid binaries
✅ Day 20. — Bandit Level 20 → 21 | Network services
✅ Day 21. — Bandit Level 21 → 22 | Cron jobs
✅ Day 22. — Bandit Level 22 → 23 | Cron and bash scripting
✅ Day 23. — Bandit Level 23 → 24 | Writing cron scripts
✅ Day 24. — Bandit Level 24 → 25 | Brute forcing and loops
✅ Day 25. — Bandit Level 25 → 26 | Restricted shells
✅ Day 26. — Bandit Level 26 → 27 | SUID privilege escalation
✅ Day 27. — Bandit Level 27 → 28 | git clone over SSH
✅ Day 28. — Bandit Level 28 → 29 | git log and git show
✅ Day 29. — Bandit Level 29 → 30 | git branches
✅ Day 30. — Bandit Level 30 → 31 | git tags
✅ Day 31. — Bandit Level 31 → 32 | git push and .gitignore ← today
⬜ Day 32. — Bandit Level 32 → 33 | coming nextFollow along with the series as I document each level, command and lesson learned.
.gitignore keeps your commits tidy. It does not keep your secrets safe, and a single flag is all it takes to prove it.