Skip to content

Running agent git commands without per-command approval prompts

A coding agent that runs git under a per-command approval prompt asks the user 19 times per session, and over 30 days the user accepted 3,829 of 3,833. Those prompts caught zero leaks in 30 days. This page explains the alternative: a stack of scanning gates that replaced the prompt, an audit log that replaced the user’s memory, and a main-session-only rule that keeps background workers from mutating git at all. The how-to for building the gates is Leak gates for coding agents.

The numbers below come from a 30-day replay of 485 pi session files (43,912 tool results, 29,820 bash calls) against the live guard cores, run 2026-10-20 on the author’s Linux dev box. Every figure is measured from that replay or from the source code described inline; asserted claims are labelled as such.

  • Baseline: 3,833 git/gh approval prompts in 30 days across 205 sessions. Four denials: two misclicks, one change-of-mind, one “check first”. Zero caught leaks. Approval was a rubber stamp.
  • After the tiered gate: a replay of the 3,648 recorded commands through the tiered gate dropped them to 496 (-86%). The dangerous-command guard shrank from 212 to 187 prompts with 24 bypasses closed.
  • Pre-push gate: a replay of 909 commits across 6 public repos blocked 14 pushes: 2 true positives (since reworded), 5 identifier-shaped refs in test fixtures and plan docs (unconfirmed), and 7 generic-wording false positives from the provenance scanner.
  • Server pre-receive: an unbypassable gate on the self-hosted Forgejo instance, covering repos push-mirrored to public GitHub. A client cannot skip it with --no-verify.
  • Tracking: an audit log of every auto-allowed command plus an agent-inventory scan (worktrees, branches, stashes, dirty trees) answers “what did agents leave behind” without a single prompt.

The baseline: what 30 days of approval prompts looked like

Section titled “The baseline: what 30 days of approval prompts looked like”

A scan of 485 pi session files (2026-09-19 to 2026-10-19) classified every bash command with the same guard cores that would later gate them:

MetricCount
Sessions with gated git/gh commands205 of 485
Gated git/gh commands3,833
Per-session average19
git commit share74%
git push share8%
User denials4
Leaks caught by the prompt0

The four denials were misclicks (“retry that commit”, “again”), a change-of-mind (“leave it” on a gh api repo edit), and a “check first” on a compound command. None of the 3,833 prompts prevented a secret or a confidential term from being committed - the pre-existing hooks (pre-commit reminder, pre-push gitleaks + blocked-term check) had already caught everything the prompt could.

Three leak classes had needed git filter-repo remediation before the study window: a secret in a committed file, internal identifiers in a public repo, and wording that disclosed where content came from.

Anthropic reports that Claude Code users accept 93% of permission prompts, and that the click-through rate is why the company built auto mode: a classifier that replaces the human in the loop for routine actions1. The author’s data matches. The confidential-write guard blocked 662 commits and 317 prose writes in the same 30-day window, yet 84% of the blocked commits were re-run byte-identically - the agent typed the same commit a second time rather than changing anything. Neither prompt changed what was committed: the git prompt was approved 3,829 times out of 3,833, and the confidential-write block was answered with the same command 84% of the time.

Five patterns from other coding agents, as of October 2026:

Claude Code auto mode. Two-tier classifier (fast single-token filter + chain-of-thought reasoning on Sonnet 4.6) replaces per-command approval. The classifier gates shell commands, web fetches, and filesystem writes outside the project directory; in-project file edits skip it entirely because version control provides the audit trail. Reported false-positive rate 0.4% on real traffic, false-negative rate 17% on known overeager actions. The “93% accept rate” number is Anthropic’s own1.

GitHub Copilot coding agent. The strongest git model in the survey. Server-side branch restriction: the agent can only push to a copilot/* branch or the existing PR branch. It cannot push to the default branch, cannot run git push directly (the platform mediates the push), cannot mark its PRs ready for review, and cannot approve or merge. The user who triggered the agent cannot be the one to approve the resulting PR. Secret scanning runs at code-generation time (CodeQL, secret scanning, dependency analysis), not as a post-hoc hook. The agent’s ephemeral VM is firewalled with a configurable egress allowlist2 3.

Gemini CLI policy engine. TOML-based rules combine tool name, command prefix, and arguments into allow/deny/ask decisions with numeric priority. Conseca, an LLM-based context-aware security checker, dynamically generates and enforces security policies. Trusted-folders mode disables project settings, .env files, and MCP servers in untrusted directories. disableYoloMode can be forced at policy level4.

Aider auto-commit with --no-verify default. The default --git-commit-verify was False until at least July 2026 (PR #5377 to change it was still open as of that date)5. That meant every aider commit ran with --no-verify, bypassing every pre-commit hook in the repo — secret scanners, linters, formatters, and test runners alike. Aider’s own docs documented the behaviour. This is the worst default in the survey for a setup where pre-commit hooks are the only push-time leak check6 7.

OpenHands OWASP audit. Score: 70/100, fail. Flagged: unsafe execution (subprocess calls), inadequate sandboxing (agent has root inside the Docker container), and excessive agency (autonomous PR creation without mandatory human review)8.

A prompt that is approved without reading adds nothing a deterministic scan of the pushed bytes cannot do, and a log of what ran records more than the user remembers.

Three tiers classify every git and gh command:

TierWhat runsExampleMechanism
RoutineAuto-allowed without a promptgit commit, git merge --ff-only, git worktree add, git push to a non-protected branchPattern match on the canonicalised command; hooks-active check
GatedPrompts the user oncegit reset --hard, git push --force, gh repo edit, git push -o leak-gate.skipFalls outside the routine tier; destructive or remote-changing
BypassRefused (blocks the command)Any mutating git/gh in a bg_bash or pi -p worker; git filter-repo without prior approvalHard block; the agent sees the reason and must take a different path

The hooksActiveFor check is the linchpin of the routine tier. Before auto-allowing a command, the gate verifies that the repo’s effective core.hooksPath points to the global hooks directory, where the pre-push gate lives. A repo using Husky (which sets .husky) or an unset hooksPath (git’s default .git/hooks) fails this check, and its commands stay gated. If the pre-push gate does not run, the prompt is the only remaining check before a push leaves the machine.

Every auto-allowed command is appended to an audit log with a timestamp, harness name, session id, working directory, and the full command text. The log is the answer to “what did the agent run when I was not looking?”

Public-vs-private classification is harder than it sounds. Before 2026-10-20, the rule was “any remote on github.com, gitlab.com, or codeberg.org counts as public.” That flagged 128 local repos as public, but only 12 of them actually were. Worse, a self-hosted Forgejo instance push-mirrored repos server-side - a local git remote showed only the Forgejo URL, but the content reached public GitHub through a mirror the local git config never saw.

The replacement reads a daily-refreshed cache that lists every public repo key, every known key, and the Forgejo-to-GitHub mirror map. The cache is built by a systemd user timer. An unknown URL on a public forge counts as public (fail-safe). A stale cache (older than 7 days) also counts unknown public-forge URLs as public.

The gate runs as the repo’s pre-push hook. It scans the exact commits being pushed, not the working tree, so a commit that was made hours ago and the agent has long forgotten about is still checked. The rules differ by target:

Every repo (public or private):

  • User-confirmed blocked terms. Terms the user has marked block via the confidential_terms tool. Enforced deterministically; a match blocks the push.
  • Sensitive filenames. .env, .pem, .p12, .kdbx, id_rsa, .viminfo, .netrc, .git-credentials, credentials JSON/YAML, and .ssh/ paths. Exceptions: .env.example, .env.sample, .env.template, .pub files, and sops-encrypted content (detected by the sops: header or ENC[AES256_GCM, prefix).
  • Gitleaks. Runs over the pushed range. Fails closed: a missing gitleaks binary or a timeout (120s) also blocks. A per-repo .gitleaks.toml or .gitleaksignore is not read because in a bare-repo pre-receive context those files do not exist; the server config is authoritative.
  • Compound-command gate. Inspects the push command itself (not the commits) for --force, --mirror, --delete, +refspec force-push syntax, and -o leak-gate.skip. These stay in the gated tier.

Public repos only (the strict rules):

  • Tainted terms (hints, not blocks). Identifier-shaped tokens from pasted text or private sources that entered the agent’s context in the last 30 days. Taint expires after 30 days. A term already published at the remote - detected by grepping the remote’s default branch - is cleared. Tainted terms are NOT blocked; they are collected as hints and passed to the LLM review, which judges whether any identifies a third party.
  • Identifier shapes. Twenty-character hex strings matching project reference patterns, and email addresses not on the author’s own domains.
  • Provenance framing. Prose that reveals where information came from: a word naming an outside party within one sentence of a word for a channel or event, plus a few fixed phrases. Applied to commit messages and prose files only; code is exempt because words like client and account are ordinary technical terms there.

The pre-push gate is a client-side hook. An agent that knows about git hooks can bypass it with --no-verify. That flag stays in the gated tier - the agent must ask the user to use it, and git push -o leak-gate.skip needs the same approval. The real backstop is the server.

When the deterministic pre-push gate blocks nothing but the push targets a public repo, and a local LLM is loaded (llmc, RTX 5090), a second pass runs: the diff is sent to a local Qwen-class model with a classification prompt. The model receives tainted terms from the session as hints (terms from internal sources, never published at the remote) and is asked only to flag confidential context - named outside parties, internal-only plans, and statements of where information came from. When the model returns leak=true, the push is blocked with the model’s reason printed verbatim.

This is the layer aimed at catching framing a regex cannot match: the deterministic rule only hits sentences built from its fixed word lists.

The review only runs when a model is already loaded. No cold start for a push. The prompt is capped at 16,000 characters; larger diffs are truncated with a line-count note. The model’s answer is parsed as a JSON verdict and the reasoning string is shown to the agent - never a line from the diff, never a name.

The only unbypassable layer. A pre-receive hook runs on the self-hosted Forgejo instance, in repos that are push-mirrored to public GitHub (plus a private canary repo). A client cannot skip it with any flag.

The hook runs gitleaks over the pushed range and checks every term from the user’s blocked-terms list. It fails closed: a scanner error, a 120s timeout, or a missing gitleaks binary all block the push. The blocked term itself is never printed in the rejection message.

Terms are synced from the dev box by a cron job that copies the union of the user’s blocked terms into the stack’s SOPS-encrypted config.

For repos mirrored to GitHub, GitHub’s own push protection runs as a final layer. It is limited to high-confidence patterns (GitHub tokens, AWS keys, and a few others) and needs a GHAS license for private repos. It catches what the earlier layers miss, but it is not a design dependency - the server pre-receive runs before the mirror push, so a block there means GitHub never sees the push.

A background pi -p worker or a bg_bash command cannot mutate git. The tiered gate’s bypass tier blocks every mutating git/gh command in a headless worker - commit, push, merge, worktree add, branch delete, everything. The worker edits files; the main session commits and pushes.

Why: a worker that commits and pushes autonomously creates a pipeline with no human in the loop at any point. The main session’s agent can see the diff, the pre-push gate can run, and the LLM review can flag a suspicious sentence. A worker sees none of that. The rule was decided 2026-10-20 after two incidents where a headless worker pushed changes the main session had not reviewed.

The prompt’s one real function was telling the user what the agent had done. Four mechanisms replace that:

Audit log. A JSON-lines file records every auto-allowed command: timestamp, harness name, session id, working directory, full command text. Append-only.

agent-inventory. A read-only shell script (agent-inventory) sweeps every local repo for git state that outlives a session: extra worktrees, unmerged branches, unpushed commits, stashes, dirty trees. Each item carries age, a details field, and - when the audit log has a match - the session that created it. Flags: --session <id> to scope to one session, --stale 7 for items idle a week or more, --json for machine output. A stale worktree from a forgotten session prints the session id, the age, and the branch.

Session report. At session start, the harness appends a summary to the session’s ledger entry: repos touched, git commands run, worktrees created.

Naming convention. Agent-created worktrees live at <repo>/.worktrees/<card>-<slug> on branch agent/<card>-<slug>, so a bare git worktree list shows at a glance which are agent-owned and which kanban card they belong to.

  • Deliberate exfiltration by a compromised model. The pre-push gate and server pre-receive catch secrets and blocked terms. They do not catch a model that steganographically encodes data into variable names or comment phrasing. No coding-agent harness addresses this today.

  • A worker that finds another way to mutate git. The bypass tier blocks the known git and gh subcommands. A worker could write a shell script that calls git commit through an alias or an indirect path. The rule is a policy backstop, not a sandbox.

  • Pre-commit hooks as a security boundary. The pre-commit hook in this setup is a non-blocking reminder (it prints what the agent is about to commit). It does not block. The pre-push gate is the first blocking layer.

  • Files the agent writes but never commits. The confidential-write guard (a separate extension) screens every write for blocked terms, tainted identifiers, and sensitive filenames. That guard is described in Guard extensions that enforce prose and process rules in a coding agent.

The replay numbers (30 days, 485 sessions) with each gate applied:

GateBeforeAfterDelta
Git/gh approval prompts3,648496-3,152 (-86%)
Dangerous-command prompts212187-25 (-12%, 24 bypasses closed)
Pre-push gate (909 public commits replayed)-14 blocked pushes-
Blocked: secrets (gitleaks), true positives-2-
Blocked: identifier-shaped refs, unconfirmed-5-
Blocked: provenance framing, false positives-7-
Server pre-receive (canary only)-2 blocked (canary terms)-

The pre-push gate’s 14 blocks from 909 commits (1.5%) include 2 true positives (secrets in commit messages, since reworded) and 7 false positives from the provenance scanner’s generic-wording matches. The other 5 blocks are identifier-shaped refs in test fixtures and plan docs - unconfirmed whether any names a real internal project. Tainted terms from internal sources are hints passed to the LLM review, not deterministic blocks.

The agent-inventory scan, run on demand and in a per-turn session report, surfaces items the prompt would never have shown: a worktree from a session that ended three days ago, a branch with unpushed commits on a repo the agent visited once, a dirty tree from an interrupted rebase.

Agent writesa commitConfidential-writeguard (write time)blocks blocked terms,tainted ids, sensitive filesTiered git gate(approval policy)Pre-push gate(client-side hook)routine: auto-allows;gated: prompts;bypass: blocksLLM review(local model, public only)every repo: blocked terms,sensitive files, gitleaks;public: +taint, provenanceServer pre-receive(unbypassable)public targets: blockson a leak verdictGitHub pushprotection (last resort)public-mirrored repos:gitleaks + blocked termsPublic remotehigh-confidencesecrets only
  1. Confidential-write guard catches blocked terms, tainted identifiers, and sensitive filenames at write time, before a commit exists.

  2. Tiered git gate replaces the per-command prompt. Routine commands auto-run with the pre-push gate as the backstop. Destructive or remote-changing commands still prompt. Workers are blocked from all mutating git.

  3. Pre-push gate scans the exact commits being pushed. Every repo gets gitleaks, blocked terms, and sensitive-file checks. Public repos add taint, identifier shapes, and provenance-framing detection.

  4. LLM review runs when the pre-push gate finds nothing but the target is public and a local model is loaded. Blocks on a leak verdict; skipped when no model is loaded.

  5. Server pre-receive is the unbypassable layer. A client cannot skip it with any flag. Runs on the Forgejo instance for repos mirrored to public GitHub.

  6. GitHub push protection catches high-confidence secrets that survived every earlier layer. Not relied upon; runs after the mirror push, which only happens if the server pre-receive passed.

ClaimHow it was checkedSource
3,833 git/gh prompts in 30 days (measured); 3,648 replayed through tiered gatePython classifier over 485 session JSONL files (43,912 tool results)Private measurement notes: 00-baseline-and-constraints.md
74% git commit, 8% git pushSame replay, subcommand histogramPrivate measurement notes: 00-baseline-and-constraints.md
4 denials, 0 leak catchesSame replay, user-message follow-up classifierPrivate measurement notes: 00-baseline-and-constraints.md
84% identical re-runs after confidential-write blockReplay, plain string comparison of the re-issued commandPrivate measurement notes: 00-baseline-and-constraints.md
Claude Code 93% acceptance rateAnthropic auto mode blog post (2026-03-25)https://www.anthropic.com/engineering/claude-code-auto-mode
Aider --no-verify defaultAider docs + PR #5377 (open as of July 2026)https://aider.chat/docs/git.html ; https://github.com/Aider-AI/aider/pull/5377
Copilot branch restriction, cannot push to mainGitHub docs (risks and mitigations)https://docs.github.com/en/copilot/concepts/security-governance-and-network-settings/risks-and-mitigations
Pre-push replay: 909 commits across 6 public repos, 14 blocks (2 TP, 5 unconfirmed, 7 FP)Re-scan of public-repo push history against the pre-push gate corePrivate measurement notes: 00-baseline-and-constraints.md
Tiered gate replay: 3,648 -> 496 (-86%)Replay of the 30-day command stream against the tier-matcherPrivate measurement notes: 00-baseline-and-constraints.md
rm-guard: 212 -> 187, 24 bypasses closedReplay of bash calls against the dangerous-command guard core after bypass fixesPrivate measurement notes: 00-baseline-and-constraints.md
128 repos flagged public, 12 actually publicRepository listing + visibility-cache comparisonPrivate measurement notes: 00-baseline-and-constraints.md
Server pre-receive blocks canary termsPush of a commit containing the canary term to the canary repo over Forgejo API (bypasses client hooks)Forgejo compose stack config, leak gate section
LLM review in the source code blocks on leak=true (no replay was ever run)Read from .config/git/hooks/lib/prepush-gate.ts llmReview() + main: “BLOCKED - the local model flagged”Source: prepush-gate.ts
  1. Anthropic, “How we built Claude Code auto mode: a safer way to skip permissions,” Anthropic Engineering Blog, 2026-03-25. https://www.anthropic.com/engineering/claude-code-auto-mode ↩ ↩2

  2. GitHub, “Risks and mitigations for GitHub Copilot cloud agent,” GitHub Docs. https://docs.github.com/en/copilot/concepts/security-governance-and-network-settings/risks-and-mitigations ↩

  3. Rahul Zhade, “How GitHub’s agentic security principles make our AI agents as secure as possible,” GitHub Blog, 2025-11-25. https://github.blog/ai-and-ml/github-copilot/how-githubs-agentic-security-principles-make-our-ai-agents-as-secure-as-possible/ ↩

  4. Google, “Policy engine,” Gemini CLI Docs. https://geminicli.com/docs/reference/policy-engine/ ↩

  5. Sarthak816, “fix: enable git pre-commit hooks by default (—git-commit-verify),” Aider-AI/aider PR #5377, 2026-07-01. https://github.com/Aider-AI/aider/pull/5377 ↩

  6. Aider, “Git integration,” Aider Docs. https://aider.chat/docs/git.html ↩

  7. “Default runtime commits bypass pre-commit hooks via —no-verify,” Aider-AI/aider issue #5057, 2026-04-21. https://github.com/Aider-AI/aider/issues/5057 ↩

  8. “OWASP Agentic AI Security Assessment for OpenHands,” OpenHands/OpenHands issue #13378, 2026-03-13. https://github.com/OpenHands/OpenHands/issues/13378 ↩