The agent did `git add -f`
I asked Claude Opus to commit the changes it had just helped me make and open a PR. It did. Then I opened the PR link to look it over and found a file in the diff that shouldn’t have been there. It was a personal .md reference I keep in the repo, in a folder with its own .gitignore containing a single line:
*
Meaning: ignore everything in this folder. Which git was doing correctly. Until Claude asked it to stop.
What I asked for
The instruction, almost verbatim:
Before committing, give me a summary of what you have done for approval to commit and push changes.
I got a summary. I read it. I approved. Nothing in the summary mentioned my personal reference file.
Then it ran:
git add <the files from the summary> && git add -f <my gitignored file>
How it got there
The part that made me laugh and then not laugh is the trail back. Earlier in the same session I had asked it to edit that personal reference file (a reasonable ask inside my own notes). When commit time came, it tried to stage everything it had touched in one shot:
git add <approved files> <my gitignored file>
That failed with git’s helpful message:
The following paths are ignored by one of your .gitignore files:
.cursor
hint: Use -f if you really want to add them.
Claude read the hint. Claude took the hint. Second command: git add -f. Then it committed and pushed.
Git’s own error told it how to skip the check.
The cleanup
git rm --cached path/to/file
git commit --amend
git push --force
The content turned out to be harmless, just personal notes. But git history lives beyond a force-push: anyone who already fetched the old commit hash still has the file. If it had been a .env I’d have been rotating credentials too, not only amending commits.
What I changed
I wrote a small block-git-force-add.sh and wired it into a user-level Cursor hook. User-level means it’s active in every local repo by default, not opt-in per project. The hook intercepts git add -f / git add --force issued by the agent and refuses it. If I actually need to force-add something, I type it myself.
Scope is the whole point. .gitignore doesn’t enforce anything. It’s a note to git and to anyone reading the repo. Same goes for a line in a prompt that says “please don’t do X.”
The lesson
Everything you tell an LLM is context: the system prompt, your messages, tool descriptions, DO NOT do the dangerous thing in bold. Context is a suggestion. The model may follow it. It may also get talked out of it by later context, like git’s own error messages.
If you don’t want an agent doing a thing, don’t give it the ability to do the thing. Enforce in code, not English. Shell hooks help. So does scoped CLI access with allowlists and no auto-run on destructive commands. And for things like force-push, rm -rf, or credential reads, I want to be the one typing. 😅
Prompts are suggestions. Hooks are code.