Skip to content
Agentic Control Plane

Stop your AI agent from rewriting your git history — in three steps

David Crowe · 6 min read
governance defense-in-depth git-safety version-control

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 pointer
  • git push --force (and --force-with-lease) — overwrites remote history
  • git clean -fd (or -fdx) — deletes untracked files
  • git filter-repo / git rebase -i — rewrites historical commits
  • git checkout . / git restore (with no path) — discards working-tree changes
  • git 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 curl command (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.

AgenticControlPlane.com

Get the next post
Agentic governance, AgentGovBench updates, the occasional incident post-mortem. One email per post. No marketing fluff.
Share: Twitter LinkedIn
Related posts

← back to blog