Stop your AI agent from rewriting your git history — in three steps
A developer reported on Hacker News that Claude Code in their repo had been running git reset --hard origin/main every ten minutes inside an automation loop — silently discarding their uncommitted work each cycle. By the time they noticed, hours of edits were gone. There’s no --undo for git reset --hard.
The same week, a Cline issue documented the agent force-pushing during a merge conflict, overwriting a teammate’s remote work and erasing live website templates. A separate Claude Code thread reported the agent running git filter-repo --strip-blobs-bigger-than 500K --force, deleting four production files from every commit in the repository’s history.
These aren’t outliers. There are at least a dozen open issues across anthropics/claude-code, cline/cline, and cursor forums describing variants of the same pattern: agent decides to “clean up,” runs a destructive git command, history disappears.
What an agent has the power to do in your repo
Modern AI agents run shell commands as the user that launched them. If you’ve authenticated git push once in your terminal, the agent can git push --force. If you’ve configured a remote, the agent can rewrite that remote’s history. The model doesn’t know which commits are months of teammate work and which are scratchpad — it’s all just hash strings to a token predictor.
The destructive git verbs that need a gate:
git reset --hard— destroys uncommitted work AND moves the branch pointergit push --force(and--force-with-lease) — overwrites remote historygit clean -fd(or-fdx) — deletes untracked filesgit filter-repo/git rebase -i— rewrites historical commitsgit checkout ./git restore(with no path) — discards working-tree changesgit branch -D— force-deletes a local branch
Any of these, called at the wrong moment, costs work that doesn’t restore from git reflog if the reflog itself has been touched.
Irreversible actions live one tool call away
Git history rewrites are a special class of irreversible: technically the data is recoverable from the reflog or unreachable objects for ~30 days… if those caches haven’t been pruned, if the agent didn’t run git gc --prune=now, if the user is sophisticated enough to know what to look for. In practice, most devs treat a force-push or hard-reset as final.
The Cline force-push incident specifically destroyed a production website template. The Claude Code reset-loop incident destroyed hours of uncommitted edits. The git-filter-repo incident destroyed historical context across the whole project. None of these were recoverable in a way that didn’t require expensive forensics.
The probability of one of these in your repo this quarter — across your team, across your AI agent fleet — is not low.
Three steps that put a gate between your agent and git
Step 1 — Install the hook
For Cursor, Claude Code, or Codex CLI:
curl -sf https://agenticcontrolplane.com/install.sh | bash
Every shell command the agent runs goes through ACP’s PreToolUse hook before execution. The model can’t route around it — Cursor / Claude Code / Codex enforces the hook from the host side, regardless of what the model wants.
Step 2 — Deny destructive git verbs by default
ACP classifies Bash sub-commands by binary, so git-prefixed calls land under Bash.git. You can be more granular by writing pattern rules within Bash.git for specific argument shapes:
{
"mode": "enforce",
"tools": {
"Bash.git": {
"background": {
"permission": "deny",
"patterns": [
{ "match": "git push.*--force", "permission": "deny" },
{ "match": "git push.*-f\\b", "permission": "deny" },
{ "match": "git reset.*--hard", "permission": "deny" },
{ "match": "git clean.*-[fd]", "permission": "deny" },
{ "match": "git filter-repo", "permission": "deny" },
{ "match": "git branch.*-D", "permission": "deny" }
]
},
"interactive": {
"permission": "ask",
"patterns": [
{ "match": "git push.*--force", "permission": "ask" },
{ "match": "git reset.*--hard", "permission": "ask" },
{ "match": "git filter-repo", "permission": "ask" },
{ "match": "git rebase -i", "permission": "ask" }
]
}
}
}
}
The semantic: in background tier (cron-scheduled jobs, headless agents, anything running without you watching), destructive git operations are denied outright. In interactive tier (your live Cursor / Claude Code session), they require an explicit approval — you’ll see an ask prompt in the dashboard before the call executes.
That single rule eliminates the class of “the agent rewrote my history overnight” incidents.
Step 3 — Bind end-user identity, per request
If you’re running an agent server-side (a CI integration, a webhook handler, a fleet of background agents), bind the end-user’s identity instead of the service account’s:
from acp_governance import set_context
@app.post("/run")
def run(req, authorization: str = Header(...)):
set_context(
user_token=authorization.removeprefix("Bearer ").strip(),
agent_name="repo-maintenance",
agent_tier="background",
)
return run_agent(req)
ACP’s policy engine resolves permissions against the user’s IdP scopes. If the user doesn’t have git:force-push on the protected branch in their RBAC, the agent doesn’t either — even if the underlying credential technically allows it.
This matters for a different reason in git workflows: branch protection rules on the Git server (GitHub / GitLab) enforce push rules at the receive-pack layer, but they don’t enforce intent. ACP enforces intent at the call site, before the request even leaves your machine.
(Free fourth step) — Audit log
Every governed git operation writes a structured row regardless of decision: command, repo, branch, agent identity, tier, decision, reason, timestamp. The denied force-push is logged the same way the allowed merge is. When something rolls back oddly, you have answers, not guesses.
The total time investment
- One
curlcommand (Step 1): ~30 seconds - Six policy rules in the dashboard (Step 2): ~3 minutes
- One line in your handler (Step 3, optional for server-side agents): ~1 minute
Three to five minutes from blank slate to “an autonomous agent in this environment cannot rewrite history without an explicit policy override and an interactive confirmation.”
If you’ve ever had to explain to your team why their commits disappeared, three minutes of work removes the part where you have to explain it again.