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-inventoryscan (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”The numbers
Section titled “The numbers”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:
| Metric | Count |
|---|---|
| Sessions with gated git/gh commands | 205 of 485 |
| Gated git/gh commands | 3,833 |
| Per-session average | 19 |
git commit share | 74% |
git push share | 8% |
| User denials | 4 |
| Leaks caught by the prompt | 0 |
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.
What past leaks looked like
Section titled “What past leaks looked like”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.
Approval fatigue
Section titled “Approval fatigue”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.
What peers do
Section titled “What peers do”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.
The design: replacing prompts with gates
Section titled “The design: replacing prompts with gates”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.
The tiered git gate
Section titled “The tiered git gate”Three tiers classify every git and gh command:
| Tier | What runs | Example | Mechanism |
|---|---|---|---|
| Routine | Auto-allowed without a prompt | git commit, git merge --ff-only, git worktree add, git push to a non-protected branch | Pattern match on the canonicalised command; hooks-active check |
| Gated | Prompts the user once | git reset --hard, git push --force, gh repo edit, git push -o leak-gate.skip | Falls outside the routine tier; destructive or remote-changing |
| Bypass | Refused (blocks the command) | Any mutating git/gh in a bg_bash or pi -p worker; git filter-repo without prior approval | Hard 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?”
The visibility map
Section titled “The visibility map”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 pre-push gate
Section titled “The pre-push gate”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
blockvia theconfidential_termstool. 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,.pubfiles, and sops-encrypted content (detected by thesops:header orENC[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.tomlor.gitleaksignoreis 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,+refspecforce-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
clientandaccountare 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.
The local-model review
Section titled “The local-model review”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 server pre-receive
Section titled “The server pre-receive”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.
GitHub push protection (last resort)
Section titled “GitHub push protection (last resort)”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.
The main-session-only rule
Section titled “The main-session-only rule”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.
Tracking replacing the prompt
Section titled “Tracking replacing the prompt”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.
What is not covered
Section titled “What is not covered”-
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 committhrough 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.
Measured results
Section titled “Measured results”The replay numbers (30 days, 485 sessions) with each gate applied:
| Gate | Before | After | Delta |
|---|---|---|---|
| Git/gh approval prompts | 3,648 | 496 | -3,152 (-86%) |
| Dangerous-command prompts | 212 | 187 | -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.
Which layer catches which leak class
Section titled “Which layer catches which leak class”-
Confidential-write guard catches blocked terms, tainted identifiers, and sensitive filenames at write time, before a commit exists.
-
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.
-
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.
-
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.
-
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.
-
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.
Evidence
Section titled “Evidence”| Claim | How it was checked | Source |
|---|---|---|
| 3,833 git/gh prompts in 30 days (measured); 3,648 replayed through tiered gate | Python classifier over 485 session JSONL files (43,912 tool results) | Private measurement notes: 00-baseline-and-constraints.md |
74% git commit, 8% git push | Same replay, subcommand histogram | Private measurement notes: 00-baseline-and-constraints.md |
| 4 denials, 0 leak catches | Same replay, user-message follow-up classifier | Private measurement notes: 00-baseline-and-constraints.md |
| 84% identical re-runs after confidential-write block | Replay, plain string comparison of the re-issued command | Private measurement notes: 00-baseline-and-constraints.md |
| Claude Code 93% acceptance rate | Anthropic auto mode blog post (2026-03-25) | https://www.anthropic.com/engineering/claude-code-auto-mode |
Aider --no-verify default | Aider 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 main | GitHub 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 core | Private measurement notes: 00-baseline-and-constraints.md |
| Tiered gate replay: 3,648 -> 496 (-86%) | Replay of the 30-day command stream against the tier-matcher | Private measurement notes: 00-baseline-and-constraints.md |
| rm-guard: 212 -> 187, 24 bypasses closed | Replay of bash calls against the dangerous-command guard core after bypass fixes | Private measurement notes: 00-baseline-and-constraints.md |
| 128 repos flagged public, 12 actually public | Repository listing + visibility-cache comparison | Private measurement notes: 00-baseline-and-constraints.md |
| Server pre-receive blocks canary terms | Push 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 |
Related docs
Section titled “Related docs”- Guard extensions that enforce prose and process rules in a coding agent - the six guards that run alongside the git gate
- Keeping credentials out of coding-agent transcripts - the secret-output guard that complements the leak gate
References
Section titled “References”-
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
-
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 ↩
-
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/ ↩
-
Google, “Policy engine,” Gemini CLI Docs. https://geminicli.com/docs/reference/policy-engine/ ↩
-
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 ↩
-
Aider, “Git integration,” Aider Docs. https://aider.chat/docs/git.html ↩
-
“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 ↩
-
“OWASP Agentic AI Security Assessment for OpenHands,” OpenHands/OpenHands issue #13378, 2026-03-13. https://github.com/OpenHands/OpenHands/issues/13378 ↩