feat(secrets): git leak guard — pre-push scan blocks pushes containing secret values (#106) #119

Merged
dominik.polakovics merged 2 commits from afk/106 into main 2026-07-10 19:52:37 +02:00

The second layer of the secrets harness (#104#106 → #105/#107/#108): lab's pre-push hook now guards every ready repo and gains a server-side secret leak scan. A push whose outgoing diff carries a repo secret value — plain or base64/hex/urlencoded — is blocked, naming the secret and file, never the value.

How it works

  • Server — new POST /agent/v1/secrets/scan (run-token scoped, so an instance can only scan against its own repo): streams the uploaded patch through the #104 matcher built from the repo's freshly decrypted values. Added lines only — blocking a removal would strand the fix behind the fail-closed guard. Bounded memory on arbitrarily long lines, matcher reset at file headers (no cross-file false positives), 256 MiB cap on every path, zero-secret repos pass without decrypting. Values appear in no response, error, or log.
  • labctlsecret scan <rev-arg>... collects the outgoing range with one git log --format= -p -m and streams it up. Fail closed: findings, git failure, server unreachable, non-2xx → exit 1; clean pass → silent 0.
  • Hook — the seeder-rendered guard (marker string byte-unchanged; deployed-hook recognition depends on it) always carries the scan block. The grill-decision failure split lives in the hook: LAB_TOKEN present → scan mandatory, any failure refuses the push; absent (a human pushing by hand) → loud one-line warning, push proceeds — the human needs neither token nor labctl. Incogni checks render only when patterns exist and keep working alongside.
  • reposvcinstallGuardHook at clone completion, incogni toggles (toggle-off re-renders without patterns instead of removing), and startup reconcile for every ready repo; core.hooksPath stays pinned permanently. Startup reconcile is also how hook-content changes roll out. Seeded context files now warn agents that pushes are scanned and that recovery is rewriting the offending commits.

Acceptance criteria

  • Leaking push blocked, error names secret + file, never the value — TestSecretScanEndToEnd (real git repo → real labctl → real agentapi handler → matcher), plus per-form handler tests
  • Clean push silent; zero-secret repos pass cheaply — TestSecretScanCleanPass, TestSecretScanZeroSecrets
  • LAB_TOKEN set + server unreachable → blocked with clear error — TestSecretScanServerUnreachable, TestPrePushHook_secretScanFailsClosedWithoutLabctl
  • No LAB_TOKEN → push proceeds with warning — TestPrePushHook_secretScanSkippedWithoutToken (real git push, stub labctl proves it's the token check that skips)
  • Coexists with the incogni guard; golden updated — TestPrePushHookScript_claudeGolden, TestPrePushHook_incogniStillBlocksAlongsideSecretSkip
  • Names only, value nowhere — asserted in handler, labctl, and e2e tests

Notes

  • First commit fixes a main-branch breakage: #104 and #111 both claimed migration version 8; run_title (merged second) moves to 0009. Every store-backed test failed before this.
  • Known limits (documented in code): a secret split across non-adjacent diff lines or inside a binary blob is not matched (contiguous adjacent added lines — a pasted PEM key — do reconstruct); scanning covers the pushed range, not history.
  • An 8-angle self-review ran before shipping; it caught and fixed a real miss (++ -prefixed added lines misparsed as +++ headers and skipped) and an unbounded body drain on the zero-secret path.
  • internal/tmuxx integration tests fail in this sandbox on pristine main too (tmux sessions cannot start here) — unrelated and untouched.

Closes #106

🤖 Generated with Claude Code

https://claude.ai/code/session_014gdntmghZvF58U7uxzghgr

The second layer of the secrets harness (#104 → **#106** → #105/#107/#108): lab's pre-push hook now guards **every** ready repo and gains a server-side secret leak scan. A push whose outgoing diff carries a repo secret value — plain or base64/hex/urlencoded — is blocked, naming the secret and file, never the value. ## How it works - **Server** — new `POST /agent/v1/secrets/scan` (run-token scoped, so an instance can only scan against its own repo): streams the uploaded patch through the #104 matcher built from the repo's freshly decrypted values. Added lines only — blocking a *removal* would strand the fix behind the fail-closed guard. Bounded memory on arbitrarily long lines, matcher reset at file headers (no cross-file false positives), 256 MiB cap on every path, zero-secret repos pass without decrypting. Values appear in no response, error, or log. - **labctl** — `secret scan <rev-arg>...` collects the outgoing range with one `git log --format= -p -m` and streams it up. Fail closed: findings, git failure, server unreachable, non-2xx → exit 1; clean pass → silent 0. - **Hook** — the seeder-rendered guard (marker string byte-unchanged; deployed-hook recognition depends on it) always carries the scan block. The grill-decision failure split lives in the hook: `LAB_TOKEN` present → scan mandatory, any failure refuses the push; absent (a human pushing by hand) → loud one-line warning, push proceeds — the human needs neither token nor labctl. Incogni checks render only when patterns exist and keep working alongside. - **reposvc** — `installGuardHook` at clone completion, incogni toggles (toggle-off re-renders without patterns instead of removing), and startup reconcile for every ready repo; `core.hooksPath` stays pinned permanently. Startup reconcile is also how hook-content changes roll out. Seeded context files now warn agents that pushes are scanned and that recovery is rewriting the offending commits. ## Acceptance criteria - ✅ Leaking push blocked, error names secret + file, never the value — `TestSecretScanEndToEnd` (real git repo → real labctl → real agentapi handler → matcher), plus per-form handler tests - ✅ Clean push silent; zero-secret repos pass cheaply — `TestSecretScanCleanPass`, `TestSecretScanZeroSecrets` - ✅ LAB_TOKEN set + server unreachable → blocked with clear error — `TestSecretScanServerUnreachable`, `TestPrePushHook_secretScanFailsClosedWithoutLabctl` - ✅ No LAB_TOKEN → push proceeds with warning — `TestPrePushHook_secretScanSkippedWithoutToken` (real `git push`, stub labctl proves it's the token check that skips) - ✅ Coexists with the incogni guard; golden updated — `TestPrePushHookScript_claudeGolden`, `TestPrePushHook_incogniStillBlocksAlongsideSecretSkip` - ✅ Names only, value nowhere — asserted in handler, labctl, and e2e tests ## Notes - First commit fixes a main-branch breakage: #104 and #111 both claimed migration version 8; `run_title` (merged second) moves to 0009. Every store-backed test failed before this. - Known limits (documented in code): a secret split across non-adjacent diff lines or inside a binary blob is not matched (contiguous adjacent added lines — a pasted PEM key — *do* reconstruct); scanning covers the pushed range, not history. - An 8-angle self-review ran before shipping; it caught and fixed a real miss (`++ `-prefixed added lines misparsed as `+++` headers and skipped) and an unbounded body drain on the zero-secret path. - `internal/tmuxx` integration tests fail in this sandbox on pristine main too (tmux sessions cannot start here) — unrelated and untouched. Closes #106 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_014gdntmghZvF58U7uxzghgr
PRs #104 (repo_secrets) and #111 (run_title) merged concurrently, both
claiming migration version 8; every store open failed with "found
duplicate migration version 8". run_title merged second, so it moves to
0009 in both dialects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gdntmghZvF58U7uxzghgr
feat(secrets): git leak guard — pre-push scan blocks pushes containing secret values (#106)
All checks were successful
ci / native (pull_request) Successful in 6m10s
af9046b269
The second layer of the secrets harness: lab's pre-push hook now guards
every ready repo, not just incogni ones, and gains a secret leak scan.

Server: POST /agent/v1/secrets/scan (run-token scoped) streams the
outgoing patch through the #104 matcher built from the repo's decrypted
values — exact/base64/hex/urlencoded forms, added lines only (blocking a
removal would strand the fix behind the fail-closed guard), bounded
memory for arbitrarily long lines, matcher reset at file headers so no
cross-file false positive. Findings carry secret NAME + file + form;
the value crosses no response, error, or log. Zero-secret repos pass
cheaply without decrypting; the 256 MiB cap holds on every path.

labctl: `secret scan <rev-arg>...` collects the range with one
`git log --format= -p -m` invocation, streams it to the server, and
fails CLOSED — findings, git failure, transport error, and non-2xx all
exit 1; a clean pass is a silent 0. An early server error wins over the
SIGPIPE it inflicts on git.

Hook: the seeder-rendered guard (marker string unchanged — recognition
of deployed hooks depends on it) always carries the scan block: with
LAB_TOKEN the scan is mandatory and any labctl failure refuses the push;
without one (a human pushing by hand) it skips with a loud one-line
warning — the threat model is the sloppy agent, not the operator. The
incogni checks render only when patterns exist and keep working
alongside; a patternless hook drops the rev-list enumeration so
token-less humans are never blocked by an un-enumerable range.

reposvc: installGuardHook runs at clone completion, incogni toggles
(toggle-off re-renders instead of removing), and startup reconcile for
every ready repo; core.hooksPath stays pinned permanently. The seeded
context file now tells agents pushes are scanned and how to recover.

Closes #106

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gdntmghZvF58U7uxzghgr
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!119
No description provided.