Your Agent Will Eventually Run git reset --hard. Stop It at the Door.
Give an agent a shell for long enough and it will, one day, run a command that can't be undone. The canonical case is anthropics/claude-code#34327: an agent ran git reset --hard origin/main within the first second of a session — twice — and destroyed 12 unpushed commits plus all uncommitted work. #46444 is the untracked-file version: an auto-cleanup rm-equivalent took out ~300 files that were never committed.
We've written before about getting that work back — and you often can, if it was committed. But the cheaper move is to not lose it. Claude Code has the exact mechanism for this built in, and almost nobody uses it: a PreToolUse hook. It runs before a tool call executes and can veto it. Sixty lines is enough to catch every irreversible git/rm command an agent might reach for.
The contract, in three lines
A PreToolUse hook is just a program. Claude Code pipes it JSON on stdin before running a Bash command:
{ "tool_name": "Bash", "tool_input": { "command": "git reset --hard origin/main" }, "...": "..." }
To block, you print a small JSON object and exit 0:
{ "hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "…why, and what to do instead…"
} }
permissionDecision is one of allow / deny / ask / defer. To allow, exit 0 with no output and the normal permission flow continues. That's the whole API. (There's also a legacy "exit 2 = block" path, but the JSON form lets you hand a reason back to the model, which matters — more on that below.)
The guard
agent-guard reads that JSON and denies a curated list of commands that destroy work with no undo:
| Blocked | Why |
|---|---|
git reset --hard | discards all uncommitted changes; can strand commits |
git clean -f | permanently deletes untracked files (never-staged work is unrecoverable) |
git push --force | can clobber commits on the remote |
git branch -D | force-deletes a branch, orphaning commits only it pointed to |
git checkout/restore . | throws away uncommitted edits in the working tree |
git stash drop / clear | deletes stashed work with no undo |
git worktree remove --force | deletes a worktree with its uncommitted/committed work |
rm -rf | recursive force-delete, no trash, no undo |
When it fires, it doesn't just say "no" — it explains and points at the safe path:
$ echo '{"tool_name":"Bash","tool_input":{"command":"git reset --hard origin/main"}}' | agent-guard
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "agent-guard blocked a destructive command. `git reset --hard`
discards every uncommitted change and can strand commits. Safer: commit/stash first, or
`git reset --soft` to keep your changes staged. (set AGENT_GUARD_OFF=1 to override deliberately.)"
}
}
That reason string goes back to the model. So the agent doesn't just hit a wall — it's told to commit first or use git reset --soft, and it can course-correct on its own. A guard that only blocks teaches nothing; a guard that redirects keeps the agent moving.
The part everyone gets wrong: what it must allow
A guard that cries wolf gets disabled within a day, and then you have no guard. The discipline is to block the irreversible command and wave through its safe twin:
git reset --hard→ blocked, butgit reset --soft(keeps your changes staged) → allowedgit push --force→ blocked, butgit push --force-with-lease(refuses if the remote moved) → allowedgit clean -f→ blocked, butgit clean -n(dry-run preview) → allowedgit branch -D→ blocked, butgit branch -d(refuses to delete unmerged work) → allowedgit restore .→ blocked, butgit restore --staged file(just unstages) → allowedrm -rf build→ blocked, butrm stale.log(single file) → allowed
Our test suite is 23 cases, and more than a third of them assert that a safe command is not blocked. That ratio is the point. The day the guard blocks something legitimate is the day someone runs AGENT_GUARD_OFF=1 permanently.
Fail safe, always
A security control that breaks your workflow when it breaks is worse than none. agent-guard only ever emits a deny; every other path is allow:
- Garbage on stdin, a shape it doesn't recognize, a non-Bash tool → allow (exit 0, no output).
- An explicit escape hatch:
AGENT_GUARD_OFF=1disables it for a deliberate cleanup.
It can never cause a failure — at worst it declines to protect you. That's the only acceptable failure mode for something sitting in front of every command your agent runs.
Install
Drop the script in .claude/hooks/agent-guard and wire it up in .claude/settings.json:
{ "hooks": { "PreToolUse": [ {
"matcher": "Bash",
"hooks": [ { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/agent-guard" } ]
} ] } }
matcher: "Bash" means it only sees shell commands — zero overhead on Edit, Read, or anything else.
The honest limits
This is a seatbelt, not a vault. It matches on the command string, so a sufficiently creative invocation (a destructive command built from variables, or buried in a script it calls) can slip past — and a command that merely mentions reset --hard in an echo could trip a false deny. Both are acceptable for a guard whose job is to catch the obvious, high-frequency mistakes, not to be a sandbox. For true isolation you still want a real container or a non---dangerously-skip-permissions posture.
And the deepest fix is the one from the rescue post: make the agent commit early and often. A guard stops the catastrophic command; frequent commits make everything else recoverable. Prevention and cure are the same lesson from two ends — keep work on the recoverable side of the line.
We build and run autonomous agent fleets on Claude Code in production. If you're hardening a multi-agent setup, talk to us.
Get the tool → agent-guard in agent-ops — open source (MIT), a single dependency-free script, part of our toolkit for running coding agents in production.
Found this useful?
- You're Running Five Agents on One Repo. Who Stops Them Overwriting Each Other?
- Your Agent Did the Work, Then It Vanished. Here's How to Get It Back.
- Your Claude Code Session Is Permanently Stuck on a 400. Don't /clear — Do This.
- The One Kind of Agent Work You Can't Recover — and How to Stop Losing It
- Your Agent Reads Text You Can't See. Frisk It First.
- You're Running a Dozen Agents. Which One Is Stuck?
- What's Actually In Your Claude Code Session File (and a Tool to Read It)
- Your Agent Fleet's Worktrees Are a Minefield. Here's How to See It.
- The Agent Wrote the Code in Minutes. Reviewing It Takes Hours. Here's Why.
- Your Claude Agent Re-Reads Its Whole Prompt Every Turn. Here's the 10× You're Leaving On the Table.
- Your Agent Can Read Everything Your Shell Can. Have You Looked?
- The Loosest Line in Your Repo Is in .claude/settings.json
- The Most Expensive File in Your Repo Runs on Every Single Turn
- Your Skill's Description Is the Whole Ballgame
- Your Repo Already Knows How To Build Itself. Put That In the CLAUDE.md.
Related posts
The boring part: governance that enforces, not displays
Most "AI governance" is a dashboard. It shows you what an agent spent, what it touched, what it decided — after the fact, in a panel nobody refreshes until the bill arrives. A display is not a control. A control is the t…
Your Launch Deserves a Real Demo Video, Not a Screen Recording With Captions
Most product launches skip the demo video entirely, and the ones that don't ship one usually reach for the fastest option: a raw screen recording, maybe some auto-generated captions, posted as-is. It's better than nothin…
The MCP Ecosystem Is Growing Fast — and Quietly Becoming a Directory of Hosted Services
We [recently measured](/insights/in-the-registry-is-not-runs-2) whether the servers in the official MCP registry actually *run* (short version: 84% are remote-only, and under half of the installable ones boot clean). Whi…
Installing a Skill Usually Means Running a Stranger's Code
The pitch for Claude Code "skills" is that they're just *instructions* — a `SKILL.md` file with some markdown telling the agent how to do a task. That framing makes them feel safe to install. A prompt can't hurt you; wor…