PRD 0012: Stuck-agent recovery flow #18

Merged
didericis merged 13 commits from agent-unstuck into main 2026-05-25 04:19:52 -04:00
Showing only changes of commit c33930290f - Show all commits
+244 -104
View File
@@ -13,86 +13,200 @@ wrong in the user-intent sense, and there is no way to say so.
## Summary ## Summary
There is no dashboard for the git-gate today. The CLI ships No off-the-shelf dashboard fits the shape claude-bottle needs
`init / list / info / start / edit / cleanup` for bottles; the gate is (per-bottle, host-local, integrated into a pre-receive rejection
visible only as a sidecar in `bottle_plan.py`'s preflight rendering. with approval feeding back into the gate's own decision). Gitleaks
No `gate` subcommand exists. itself is a CLI with no UI and was declared **feature-complete** in
early 2026; the author's successor project **Betterleaks** is
explicitly "for the agentic era" but is also CLI-shaped and still
young. The closest open-source dashboard is **DefectDojo**, which
ingests gitleaks JSON but is post-hoc and org-scale — its "marked
as accepted" state does not feed back into the scanner. SaaS
dashboards (GitGuardian, TruffleHog Enterprise) ship repo content
to a vendor and were already disqualified by
`git-secret-scanning-hardening.md`.
There is also no exception mechanism. The pre-receive hook calls The git-gate ships no exception mechanism today: the pre-receive
`gitleaks git --log-opts="$range" --no-banner --redact` with no hook calls `gitleaks git --log-opts="$range" --no-banner --redact`
config path and no allowlist surface. PRD 0008 explicitly rejects with no `--config` and no `--baseline-path`, and PRD 0008
exceptions ("Bypass for trusted commits. No `[skip gitleaks]` explicitly rejects exceptions ("Bypass for trusted commits. No
trailer, no allowlist by commit hash. If the gate is bypassable it `[skip gitleaks]` trailer, no allowlist by commit hash. If the
isn't a gate."). gate is bypassable it isn't a gate.").
That non-goal is correct under its own framing — *any path the agent That non-goal is correct against the *agent* but conflates two
can take* invalidates the gate — but it conflates two distinct questions: can the *agent* bypass the gate (must be no), and can
questions: can the *agent* bypass the gate (must be no), and can the the *user* approve a narrowly-scoped exception out-of-band (could
*user* approve a narrowly-scoped exception (could be yes, under be yes). PRD 0012's recovery flow is exactly the seam where the
constraints). PRD 0012's recovery flow is exactly the seam where a user-side approval can live without giving the agent any in-band
user-side, out-of-band approval could live without giving the agent bypass.
any in-band bypass.
The design problem is therefore not "should there be exceptions" but Gitleaks does ship one native primitive that maps well to "approve
"how narrow does an exception have to be before the gate is still a this specific finding" — the **baseline file** — which is
gate." This note surveys gitleaks's native allowlist primitives, semantically a better fit for per-finding approval than the
sketches three approval-scope designs, and recommends a direction. allowlist config (a suppression *rule*). This note surveys the
dashboard landscape, the two native primitives (allowlist and
baseline), and recommends a direction.
## Question 1: Is there a dashboard / operator surface for git-gate? ## Question 1: Existing dashboards and control surfaces
No, in three senses: ### Inside claude-bottle today
- **No CLI subcommand.** `claude_bottle/cli/` has `_common, cleanup, `claude_bottle/cli/` has `_common, cleanup, edit, info, init, list,
edit, info, init, list, start` and nothing gate-specific. start` — nothing gate-specific. The gate appears only as a sidecar
`claude-bottle list` shows bottles, not their gates' state or in `bottle_plan.py`'s preflight rendering. Rejections are written
recent rejections. to the pre-receive hook's stderr (`echo "git-gate: gitleaks
- **No gate-side log surface.** Rejections are written to the rejected push to $ref" >&2`) and surface only in the agent's
pre-receive hook's stderr (`echo "git-gate: gitleaks rejected push `git push` output — nothing persists outside the container's logs.
to $ref" >&2`); the agent sees the rejection in its `git push`
output, but nothing persists outside the container's logs.
- **No upstream UI for git-gate.** gitleaks itself is a CLI; it has
no built-in dashboard. The hosted secret-scanning UIs surveyed in
`git-secret-scanning-hardening.md` (ggshield, TruffleHog Enterprise)
are SaaS products that ship repo content to a vendor — explicitly
the wrong shape for a project whose premise is sandbox isolation.
The PRD 0012 dashboard, when it exists, is the natural place for ### Native gitleaks: CLI-only, and now feature-complete
git-gate operator surface to live: list pending change requests,
show recent rejections per bottle, render the diff of any Gitleaks has no built-in dashboard or web UI. As of early 2026 the
exception-approval request. There is no reason to build a separate project has been declared **feature complete** — only security
gate dashboard. patches will be merged going forward. The original maintainer
(Zachary Rice) has moved active work to Betterleaks (below), so
any dashboard built directly against gitleaks should treat the
gitleaks surface as frozen rather than evolving.
### Betterleaks: the same author's "agentic era" successor
Started February 2026 and explicitly framed for AI agents driving
the scanner: flag-based output for low-token-overhead consumption,
parallelized Git scanning, CEL-based filtering in place of the
TOML allowlist, and a roadmap that includes LLM-assisted
classification and automatic secret revocation via provider APIs.
Still CLI-shaped — no dashboard either.
Relevant to claude-bottle in two ways:
- The upstream direction of travel is *toward* agent-driven
scanners, which makes "the bottle invokes a scanner and reports
findings up" a supported pattern rather than a hack.
- CEL is a richer expression language for filter entries than
gitleaks's selector struct, which loosens the design space for
Option B (below). If claude-bottle ever swaps gitleaks for
Betterleaks, the approval-flow design should be expressible in
both.
### Output formats: SARIF + viewers
Both gitleaks and Betterleaks can emit SARIF. That plugs into
GitHub Advanced Security's Code Scanning tab (read-only viewer
with a dismiss-as-not-a-problem state) and assorted open-source
SARIF viewers (`sarif-web-component`, Microsoft's VS Code
extension). These render findings; they do not handle approval
state or feed back into the scanner. Useful for *seeing* findings;
not useful as the approval surface.
### Findings aggregators
[**DefectDojo**](https://defectdojo.com/integrations/gitleaks) is
the closest open-source thing to "a dashboard for gitleaks." It
ingests gitleaks JSON (and ~200 other scanners), aggregates and
deduplicates, lets you triage and mark findings as accepted or
false-positive in its UI, and tracks remediation state. Designed
for org-scale: one DefectDojo instance covers many repos and
scanners.
Shape mismatch for claude-bottle:
- DefectDojo's review state is *informational* — marking a finding
as accepted in DefectDojo does not write to gitleaks's allowlist
or baseline and does not change what the gate decides on the
next push.
- It expects findings as artifacts of CI runs, not as the
rejection-cause of an in-flight push.
- A single shared instance violates the one-sidecar-per-bottle
posture; per-bottle DefectDojo instances are absurd overhead.
Useful to know it exists, especially for long-term post-hoc
finding tracking. Not the v1 answer for the in-flight approval
flow PRD 0012 needs.
A separate [JupiterOne integration](https://github.com/gitleaks-findings/gitleaks)
exists but ships findings to JupiterOne's commercial platform and
has effectively zero public adoption (0 stars, 0 forks). Mentioned
only because its repo name suggests "the dashboard" and isn't.
### SaaS dashboards (disqualified by sandbox premise)
GitGuardian / ggshield and TruffleHog Enterprise both offer
incident-triage UIs with finding-level approval state. Both ship
repo content to a vendor; already disqualified in
`git-secret-scanning-hardening.md` for a project whose entire
premise is sandbox isolation.
### Bottom line
No off-the-shelf dashboard fits claude-bottle's shape: per-bottle,
host-local, integrated into a pre-receive rejection with the
approval feeding back into the gate's own decision-making. The
nearest open-source analogue (DefectDojo) is post-hoc and
org-scale; the nearest UX (GitGuardian) is SaaS. The PRD 0012
dashboard — sharing surface with the broader stuck-agent recovery
flow — remains the right place to build this.
## Question 2: How could specific commits be approved? ## Question 2: How could specific commits be approved?
### What gitleaks gives you natively ### What gitleaks gives you natively
Gitleaks's TOML config supports an `[allowlist]` block (or Two distinct primitives, and the distinction matters for designing
`[[rules.allowlists]]` per-rule) with four selectors that can be an approval flow.
combined inside a single entry. The selectors observed in current
gitleaks (v8) are: **Allowlists** are *suppression rules* — config-level patterns that
say "ignore findings matching X." Gitleaks's TOML config supports
an `[allowlist]` block (or `[[rules.allowlists]]` per-rule) with
four selectors:
- `paths` — list of regex against file paths. - `paths` — list of regex against file paths.
- `regexes` — list of regex matched against the *finding's* matched - `regexes` — list of regex matched against the finding bytes;
bytes; on match, suppress the finding. `regexTarget` chooses `regexTarget` directs the regex at the extracted secret
whether the regex applies to the matched bytes, the surrounding (default), the entire regex match, or the whole line.
line, or the secret group only. - `stopwords` — substrings that, if present, suppress the finding.
- `stopwords` — substrings that, if present in the finding, suppress
it. Cheaper than `regexes` for literal matches.
- `commits` — explicit commit SHAs to skip entirely. - `commits` — explicit commit SHAs to skip entirely.
- `condition` — `AND` (default) or `OR` across the above selectors,
letting an entry require, e.g., both a path match *and* a content
match before suppressing.
`commits` is the bluntest tool and the easiest to misuse: a single Selectors combine with `condition = "OR"` (default; suppress if any
SHA can hide arbitrary content. `paths + regexes` (with AND) is the selector matches) or `condition = "AND"` (suppress only if all
narrowest scope: a finding is only suppressed if it lives at a match). `commits` is the bluntest tool and the easiest to misuse:
specific path *and* matches a specific byte pattern. That's the a single SHA can hide arbitrary content. `paths + regexes` with
shape that makes a per-finding exception still defensible. AND is the narrowest scope, and the form that makes a per-finding
exception still defensible.
The hook today does not pass `--config` to gitleaks. Adding it would **Baselines** are a *known-findings list* — a JSON file of
mean baking a config file into the gate image *or* mounting one in previously detected findings that gitleaks's `IsNew` function
at `start` time. The image is built per `DockerGitGate.start`, so compares against on the next scan, so only new findings get
either is mechanically straightforward. reported. The file is generated by saving a scan's JSON output and
fed back in via `--baseline-path`. The comparison checks RuleID,
description, file path, line numbers, secret content, commit, and
author/timestamp. When `--redact` is enabled, redacted Secret and
Match fields are ignored in the comparison so the baseline still
functions with redacted reports.
Detection flow is: global allowlist → rule-specific allowlist →
baseline → reported finding. Allowlist suppressions therefore win
over baseline; baseline is the last gate before report.
The hook today passes neither `--config` nor `--baseline-path`.
Wiring either in is mechanically straightforward: the gate image
is built per `DockerGitGate.start`, so the config / baseline can be
baked into the image *or* mounted in at start.
**Allowlist vs baseline for approval storage.** Both can express
"don't reject this finding," but they imply different things about
intent:
- An *allowlist* entry says "any future finding that matches this
pattern is fine." Generative: it covers findings that don't
exist yet on commits that haven't been made.
- A *baseline* entry says "this exact finding I've already seen is
fine." Specific: it pins to the bytes / location / rule of one
observed finding; a different finding on the same path on a
later commit re-triggers.
For a per-commit user approval, baseline is the better semantic
match: each approval is an attestation about one observed finding,
not a rule that pre-approves a pattern. Baseline entries can also
be diffed in PRs trivially (it's a JSON list) — they double as the
audit record.
### The design tension ### The design tension
@@ -141,40 +255,41 @@ specific placeholder like `<aws-access-key-id>`).
specifically about a real-looking token format, or the upstream specifically about a real-looking token format, or the upstream
doc requires the literal pattern). doc requires the literal pattern).
**Option B — Per-finding narrow allowlist via PRD 0012 flow.** When **Option B — Per-finding approval via PRD 0012 flow.** When the
the agent's push is rejected, the agent invokes agent's push is rejected, the agent invokes
`/request-gate-exception` (or `/request-bottle-change` with an `/request-gate-exception` (or `/request-bottle-change` with an
exception variant). The slash command POSTs to the cred-proxy exception variant). The slash command POSTs to the cred-proxy
endpoint, carrying: endpoint, carrying the gitleaks finding record (rule ID, file path,
line, redacted match) and a free-text justification ("docs example
for AWS auth flow").
- the file path that triggered the finding The user reviews the request in the dashboard, sees the file and
- the finding's matched-byte hash (not the bytes themselves, to keep the diff, and approves. The approval gets written into the gate's
the request artifact non-secret on its own) **baseline file** — the JSON list of known-OK findings the gate
- the gitleaks rule ID passes as `--baseline-path` to gitleaks. The gate restarts with
- a free-text justification ("docs example for AWS auth flow") the new baseline.
The user reviews the request in the dashboard, sees the file and the - *Property:* approved findings are pinned to the specific
diff, and approves an entry of shape `{ paths: [<exact path>], observed bytes / path / rule. A different secret on the same
regexes: [<exact-match regex over matched bytes>], condition: AND }`. path on a later commit re-triggers the gate.
The gate restarts with that config entry merged into its - *Auditability:* baseline file is JSON in git history; each PR
`.gitleaks.toml`. A future commit on the same path with a *different* approval becomes a diff to that file. The free-text
finding still hits the gate and rejects. justification lives in the PR thread per PRD 0012.
- *Fallback to allowlist for canonical cases.* If a particular
- *Property:* approved exceptions are content-locked, not commit- fixture file should be permanently understood as "examples only,"
locked. Substituting bytes on the same path triggers a fresh the user can promote a baseline entry to an `[allowlist]` rule
rejection. with `paths + regexes` AND — explicit generalization, opt-in by
- *Auditability:* the approval is a manifest diff; it lives in git the user, never by the agent.
history and in the PR conversation thread per PRD 0012. - *Open: TTL.* Should baseline entries expire? Baseline is specific
- *Open: TTL.* Should the entry expire? Plausible defaults: never by construction, so the case for expiration is weaker than for
(it's content-locked anyway), or "until the next manifest version allowlist. Lean "never" for v1; revisit if baselines balloon.
bump." Lean "never" for v1; revisit if exception lists balloon.
**Option C — Pre-flight scan with author signoff.** Run gitleaks **Option C — Pre-flight scan with author signoff.** Run gitleaks
client-side inside the bottle (as a non-gating advisory check) so client-side inside the bottle (as a non-gating advisory check) so
the agent sees findings *before* attempting the push. The slash the agent sees findings *before* attempting the push. The slash
command then includes the pre-known findings; the dashboard shows command then includes the pre-known findings; the dashboard shows
the user the finding inline rather than having to go look at the the user the finding inline rather than having to go look at the
rejection log. On approval, same Option-B-style allowlist entry rejection log. On approval, same Option-B-style baseline entry
gets added. gets added.
- *Property:* identical end-state to Option B; better UX because - *Property:* identical end-state to Option B; better UX because
@@ -188,23 +303,28 @@ gets added.
### Recommendation ### Recommendation
Default to Option A as the canonical answer ("rewrite to use a Default to Option A as the canonical answer ("rewrite to use a
placeholder"). Build Option B as the PRD 0012 exception path, scoped placeholder"). Build Option B as the PRD 0012 exception path,
narrowly: `paths + regexes` with AND, no `commits` selector exposed storing approvals in the gate's **baseline file** (not in an
to the approval flow. Defer Option C to a follow-up; it's an allowlist rule). Baseline is the right primitive because each
ergonomic win, not a security property. approval is an attestation about one observed finding, not a
generative pattern. Allowlist promotion is a separate, user-
initiated escalation for cases that genuinely deserve patterning.
The `commits` selector is never exposed to the approval flow under
either path — it hides arbitrary content. Defer Option C to a
follow-up; it's an ergonomic win, not a security property.
This puts the answer to PRD 0012's open question as: This puts the answer to PRD 0012's open question as:
- Same recovery shape (`/request-bottle-change`), distinguishable - Same recovery shape (`/request-bottle-change`), distinguishable
request type. The dashboard renders an exception request request type. The dashboard renders an exception request
differently from a manifest-change request because the *diff* differently from a manifest-change request because the *diff*
being approved is to the gate's allowlist, not to the manifest. being approved is to the gate's baseline file, not to the
- Exceptions are expressed as `(path, content-pattern)` pairs, not manifest.
commit SHAs. Re-pushing different bytes on the same path - Exceptions are expressed as baseline-file entries — finding-
re-triggers the gate. specific JSON records — not commit SHAs or regex patterns.
- The approval is recorded twice for audit: in the PR thread (free- - The approval is recorded twice for audit: in the PR thread
text), and as a versioned diff to the gate's allowlist config (or (free-text justification), and as a versioned diff to the
the manifest field that materializes into it). baseline file (which is committed alongside the manifest).
## Cross-references ## Cross-references
@@ -218,12 +338,32 @@ This puts the answer to PRD 0012's open question as:
## Sources ## Sources
- [gitleaks configuration documentation](https://github.com/gitleaks/gitleaks#configuration) - [gitleaks repository](https://github.com/gitleaks/gitleaks) —
— `[allowlist]` selectors (`paths`, `regexes`, `stopwords`, `[allowlist]` selectors (`paths`, `regexes`, `stopwords`,
`commits`, `regexTarget`, `condition`). `commits`, `regexTarget`, `condition`); also home of the
feature-complete notice.
- [Gitleaks allowlists & baselines (DeepWiki)](https://deepwiki.com/gitleaks/gitleaks/4.4-allowlists-and-baselines)
— detailed walk-through of the allowlist selector struct, the
baseline file format, the `IsNew` comparison logic, and the
global→rule→baseline detection order. Primary source for the
allowlist-vs-baseline distinction this note rests on.
- [Betterleaks (GitHub)](https://github.com/betterleaks/betterleaks)
— Zachary Rice's successor project; CEL filtering, agent-driven
output design, roadmap for LLM-assisted classification.
- [Help Net Security on Betterleaks](https://www.helpnetsecurity.com/2026/03/19/betterleaks-open-source-secrets-scanner/)
and [The New Stack](https://thenewstack.io/betterleaks-open-source-secret-scanner/)
— context on the "agentic era" framing and why gitleaks froze.
- [DefectDojo gitleaks parser](https://defectdojo.com/integrations/gitleaks)
— JSON ingest, finding triage UI, accept/false-positive state.
Open-source, generic, post-hoc; informational state only —
marking a finding as accepted does not feed back into the
scanner. Shape mismatch for in-flight per-bottle approval.
- [gitleaks-findings/gitleaks](https://github.com/gitleaks-findings/gitleaks)
— JupiterOne integration, not a dashboard. Listed because the
repo name is misleading.
- [AWS example access key (`AKIAIOSFODNN7EXAMPLE`)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html) - [AWS example access key (`AKIAIOSFODNN7EXAMPLE`)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html)
— documented placeholder safe to use in examples without — documented placeholder safe to use in examples without
triggering most secret scanners. triggering most secret scanners.
- `claude_bottle/git_gate.py` — pre-receive hook implementation - `claude_bottle/git_gate.py` — pre-receive hook implementation.
(`gitleaks git --log-opts="$log_opts" --no-banner --redact`, no Today: `gitleaks git --log-opts="$log_opts" --no-banner
`--config` argument today). --redact`; no `--config`, no `--baseline-path`.