feat: adversarial commit-reviewer + mechanical pre-commit gate#18
feat: adversarial commit-reviewer + mechanical pre-commit gate#18
Conversation
Two-layer code review layered on top of commits: 1. **Mechanical gate** — `scripts/pre-commit-hook.sh` installed at `.git/hooks/pre-commit` via `scripts/install-git-hooks.sh`. Blocks a commit if it would fail: - `cargo fmt --all -- --check` (only when Rust files touched) - `cargo clippy --workspace --all-targets -- -D warnings` (ditto) - `gitleaks protect --staged --config .gitleaks.toml` Deliberately excludes `cargo test` — too slow per-commit. The existing `scripts/pre-push.sh` covers full tests and is installed alongside at `.git/hooks/pre-push`. 2. **Semantic reviewer** — a `commit-reviewer` subagent (`.claude/agents/commit-reviewer.md`) invoked via the `/review-commit` slash command (`.claude/commands/review-commit.md`). The agent prompt is tuned HARD against style nits and "could-be-tighter" feedback — it only emits BLOCK (real bug), INCONSISTENCY (sibling code disagrees), or TEST-QUALITY findings. Zero-finding reviews are a valid output. 3. **Automation** — `scripts/claude-post-commit.sh` is a Claude Code PostToolUse hook (wire it via the example at `.claude/settings.local.json.example` — copy to `settings.local.json` to enable). When the Bash tool runs `git commit`, the hook writes the diff to `.claude/pending-reviews/<sha>.md` and touches `.claude/REVIEW_PENDING`. On Claude's next turn it notices the flag and runs `/review-commit`, which spawns the subagent. Findings come back to the main session for judgment — nothing is auto-applied, and the amend-vs-fixup decision stays with the human (/Claude) on a per-finding basis. Gitignore additions: - `.claude/REVIEW_PENDING` — transient trigger file - `.claude/pending-reviews/` — queued diffs awaiting review - `.claude/settings.local.json` — per-machine Claude Code config Rationale for splitting the subagent out of the hook: Calling Claude from inside a Claude Code session (nested sessions) is unpredictable and expensive. Instead the hook is a cheap shell-only signaller; the real review happens in-session via the normal Agent tool, which keeps cost/latency under the main session's control. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The reviewer subagent caught three real issues in its own introducing commit (plus one false positive about a pipefail idiom that empirically works). Fixing the real ones: **BLOCK: claude-post-commit.sh grep fired on non-commits.** The old `grep -q 'git commit'` matched `echo "git commit"`, `git commit --dry-run`, `git commit --help`, and any Bash invocation that merely quoted the substring. Replaced with: - JSON parse of `.tool_input.command` via `jq` (bail if jq missing — don't fall back to permissive grep) - case-block excluding `--dry-run` / `--help` / `man git-commit` - regex requiring `git commit` as a standalone action (start of string, after `&&`/`;`/`||`) - a `.last-reviewed-sha` state file so a second hook invocation on the same HEAD is a no-op (backstop for anything the JSON guard misses). **BLOCK: pre-commit gitleaks silently skipped when missing.** The original script printed `note "gitleaks not installed — skipping"` and let the commit through — effectively a default-on bypass, which CLAUDE.md "Do NOT bypass the scan" explicitly rejects. Now fails closed: if no `gitleaks` binary is found anywhere we check, the commit is refused. Explicit override via `PRE_COMMIT_SKIP_GITLEAKS=1` for the rare case where that's needed (and the commit message should document why). **INCONSISTENCY: install-git-hooks.sh silent no-op on pre-existing hook.** The original `if [[ -e "$target" ]] && [[ ! -L "$target" ]]; then echo '!'; return 0; fi` left any pre-existing non-symlink hook in place, returned 0, and let the user/CI believe the install had succeeded. Now backs the old hook up to `.git/hooks/<name>.backup-<ts>` and installs the new link. One-step restore available if the old hook was intentional. **Rejected finding (for the record):** the reviewer also flagged the `if ! cargo clippy | tail` pipefail idiom as silently swallowing failures. Empirically verified it works — pipefail sets pipeline rc to clippy's rc, `!` flips, `if` enters branch, `fail` runs. The reviewer's writeup talked itself in a circle; landed correct but ambiguous. Worth tracking as a "be more decisive" prompt tweak. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds local git-hook enforcement and a Claude Code “commit reviewer” workflow to catch mechanical issues (fmt/clippy/gitleaks) pre-commit and semantic issues post-commit, aligning with the repo’s public-repo + secret-scanning constraints.
Changes:
- Introduces a pre-commit hook script that conditionally runs Rust checks and always runs staged-only gitleaks.
- Adds an idempotent hook installer to wire pre-commit + existing pre-push checks.
- Adds a Claude Code PostToolUse hook script +
/review-commitcommand +commit-revieweragent prompt, plus ignores for local Claude state.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/pre-commit-hook.sh | New pre-commit mechanical gate (fmt/clippy conditional, gitleaks staged scan). |
| scripts/install-git-hooks.sh | New installer to symlink repo hook scripts into .git/hooks/. |
| scripts/claude-post-commit.sh | New Claude PostToolUse hook to queue commit diffs for subagent review. |
| .gitignore | Ignores local Claude hook config/state and pending review artifacts. |
| .claude/settings.local.json.example | Example local Claude hook configuration to run the post-commit script. |
| .claude/commands/review-commit.md | Adds /review-commit command to spawn the commit-reviewer subagent. |
| .claude/agents/commit-reviewer.md | Adds the commit-reviewer agent spec/prompt and output contract. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| # Only run the rust checks if the commit touches Rust files. Docs / | ||
| # config / scripts commits shouldn't pay the clippy tax. | ||
| STAGED_RUST="$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.rs$|Cargo\.(toml|lock)$' || true)" |
There was a problem hiding this comment.
Codex integration sweep: addressed in #38 without editing this PR branch. FnoxClient now wraps subprocess waits in a bounded timeout with fast tests for hung get/set paths.
| @@ -0,0 +1,56 @@ | |||
| #!/usr/bin/env bash | |||
| # install-git-hooks.sh — idempotent installer for the repo's git hooks. | |||
| # Safe to run multiple times. Won't clobber any hook already in place. | |||
There was a problem hiding this comment.
Codex integration sweep: acknowledged. I am leaving this PR branch untouched per the parallel-agent boundary; this remains actionable for the PR owner or a follow-up unless it is superseded by #38.
| # Extract the actual command from the JSON. Require jq — it's present | ||
| # on every platform we support via the installer's toolchain. If jq is | ||
| # missing we bail cleanly rather than falling back to a permissive | ||
| # grep that fires on text-about-commits. | ||
| if ! command -v jq >/dev/null 2>&1; then |
There was a problem hiding this comment.
Codex integration sweep: acknowledged. I am leaving this PR branch untouched per the parallel-agent boundary; this remains actionable for the PR owner or a follow-up unless it is superseded by #38.
…ning (#44) Squash-merge of integration/super-combined — 4 weeks of feature work + cross-PR security fixes + codex agent's hardening, all green CI (14/14 checks). ## Features landing - **fnox secret-resolver integration** (#15) + FnoxClient subprocess wrapper (#21) - **Adversarial commit-reviewer + mechanical pre-commit gate** (#18) - **{{secret:NAME}} substitution engine** in security-proxy URL/headers/body (#19) - **Per-secret destination allowlist** (#22) — RFC §11.1 attack defense - **!secure chat commands** (set/list) on Telegram (#20), Matrix (#28), WhatsApp (#31) - **zeroclawed-mcp** scaffold — agent-facing secret discovery server (#23) - **install.sh wires MCP** into Claude Code agent configs (#26) - **zeroclawed-secret-paste** — localhost web UI for one-shot secret input (#34) - **Bulk paste UI** — .env-style multi-secret onboarding with per-line results - **LAN-friendly defaults** — bind 0.0.0.0 + RFC 1918 Origin acceptance - **WhatsApp HMAC verification** (was always-true placeholder before — codex hardening) ## Security fixes folded in - /vault/:secret bearer auth + 127.0.0.1 default bind (#39) - URL-embedded secrets honor destination allowlist (#41) - Paste-flow: bearer URL only at debug, fnox set via stdin not argv (#40) - Paste-flow: graceful shutdown, exit-on-submit, reject Origin: null (#43) - Subprocess timeouts + kill_on_drop on FnoxClient - BrokenPipe-tolerant stdin write (Linux CI surface) - Header-value log redaction - OneCLI bound to 127.0.0.1 by default - Sanitized real API token + Telegram IDs from sample configs (#36) ## Architecture / refactors - Consolidated onecli binary into security-proxy (#17) - Hardcoded vault URL removed from onecli-client - security-proxy resolver wired into hot path - Extracted build_app router; migrated /vault/:secret route - !secure parser uses split_whitespace (was splitn), audit-logs invocations ## Test coverage added - security-proxy substitution engine + body/headers tests - onecli-client retry + Http(_) variant + adversarial fallthrough suite - onecli-client client.rs rewritten from tautologies to wiremock-backed - config/validator coverage (was zero, now 290-line module covered) - 16 zeroclawed-secret-paste tests including bulk-mode cases ## Docs / RFCs - agent-secret-gateway holistic architecture - consolidation-findings (what #28 must address) - secret-input-web-ui RFC (input-only, new-by-default) - browser-harness integration spike - test-quality-audit Round 1+2+3 (host-agent + zeroclawed priority files) ## Codex agent's hardening cherry-picks - Subprocess timeouts on fnox calls - map_spawn_error helper - Validator hardening + atomic-counter digest race fix - WhatsApp HMAC implementation + tests - proxy header-value log redaction CI: all 14 checks green at squash time. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
Subsumed by #44 (squashed to |
…ning (#44) Squash-merge of integration/super-combined — 4 weeks of feature work + cross-PR security fixes + codex agent's hardening, all green CI (14/14 checks). ## Features landing - **fnox secret-resolver integration** (#15) + FnoxClient subprocess wrapper (#21) - **Adversarial commit-reviewer + mechanical pre-commit gate** (#18) - **{{secret:NAME}} substitution engine** in security-proxy URL/headers/body (#19) - **Per-secret destination allowlist** (#22) — RFC §11.1 attack defense - **!secure chat commands** (set/list) on Telegram (#20), Matrix (#28), WhatsApp (#31) - **zeroclawed-mcp** scaffold — agent-facing secret discovery server (#23) - **install.sh wires MCP** into Claude Code agent configs (#26) - **zeroclawed-secret-paste** — localhost web UI for one-shot secret input (#34) - **Bulk paste UI** — .env-style multi-secret onboarding with per-line results - **LAN-friendly defaults** — bind 0.0.0.0 + RFC 1918 Origin acceptance - **WhatsApp HMAC verification** (was always-true placeholder before — codex hardening) ## Security fixes folded in - /vault/:secret bearer auth + 127.0.0.1 default bind (#39) - URL-embedded secrets honor destination allowlist (#41) - Paste-flow: bearer URL only at debug, fnox set via stdin not argv (#40) - Paste-flow: graceful shutdown, exit-on-submit, reject Origin: null (#43) - Subprocess timeouts + kill_on_drop on FnoxClient - BrokenPipe-tolerant stdin write (Linux CI surface) - Header-value log redaction - OneCLI bound to 127.0.0.1 by default - Sanitized real API token + Telegram IDs from sample configs (#36) ## Architecture / refactors - Consolidated onecli binary into security-proxy (#17) - Hardcoded vault URL removed from onecli-client - security-proxy resolver wired into hot path - Extracted build_app router; migrated /vault/:secret route - !secure parser uses split_whitespace (was splitn), audit-logs invocations ## Test coverage added - security-proxy substitution engine + body/headers tests - onecli-client retry + Http(_) variant + adversarial fallthrough suite - onecli-client client.rs rewritten from tautologies to wiremock-backed - config/validator coverage (was zero, now 290-line module covered) - 16 zeroclawed-secret-paste tests including bulk-mode cases ## Docs / RFCs - agent-secret-gateway holistic architecture - consolidation-findings (what #28 must address) - secret-input-web-ui RFC (input-only, new-by-default) - browser-harness integration spike - test-quality-audit Round 1+2+3 (host-agent + zeroclawed priority files) ## Codex agent's hardening cherry-picks - Subprocess timeouts on fnox calls - map_spawn_error helper - Validator hardening + atomic-counter digest race fix - WhatsApp HMAC implementation + tests - proxy header-value log redaction CI: all 14 checks green at squash time. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Summary
Two-layer pre-merge check for work Claude produces:
scripts/pre-commit-hook.sh→.git/hooks/pre-commitviascripts/install-git-hooks.sh).Blocks any commit that would fail
cargo fmt --check,cargo clippy --workspace -- -D warnings, orgitleaks protect --staged.Rust checks only run when Rust files are staged. Gitleaks fails
closed — if the binary isn't installed, the commit is refused
rather than silently bypassed. The existing
scripts/pre-push.sh(full tests + loom) gets installed alongside as
.git/hooks/pre-push.commit-reviewersubagent(
.claude/agents/commit-reviewer.md) invoked via the/review-commitslash command (.claude/commands/review-commit.md).Prompt tuned to reject style nits and "could-be-tighter" feedback
— emits only BLOCK (real bug), INCONSISTENCY (sibling code
disagrees), or TEST-QUALITY findings. Zero-finding reviews are
valid and expected to be common.
scripts/claude-post-commit.sh, wired via.claude/settings.local.json.example) fires after Claude Code'sBash tool runs
git commit, parses the JSON tool input withjq,excludes
--dry-run/--help/non-commit forms, checks HEADactually advanced since the last queued review, and writes the diff
to
.claude/pending-reviews/<sha>.md. Next Claude turn reads theflag file and runs
/review-commit. Nothing is auto-applied —findings return to the main session for judgment. Amend vs. stack
a new commit decided per-finding.
Dogfood evidence
The reviewer was run against its own introducing commit (1aba88a).
It found 3 real issues + 1 false positive:
grep -q 'git commit'matched too broadly(matched
echo "git commit",--dry-run, etc.). Fixed with JSONparse + dry-run exclusion + prior-SHA backstop.
note "gitleaks not installed — skipping"was a default-on bypass, contradicting CLAUDE.md. Fixed to fail
closed with an explicit
PRE_COMMIT_SKIP_GITLEAKS=1override.install-git-hooks.shleft pre-existingnon-symlink hooks in place and returned 0 silently. Fixed to back
them up and replace.
if ! cmd | tailpipefailidiom. Verified empirically it works; noted as a "be more decisive"
prompt tweak for future reviewer iterations.
All three fixes live in the follow-up commit in this PR (stacked, not
amended — demonstrating the intended workflow).
Test plan
bash -non all three scriptsinstall-git-hooks.shis idempotent (symlinks identical → no-op)/tmp/pipefail_test.shempirically confirmed the if-!-pipe idiom workssettings.local.json.exampletosettings.local.json, commit something, confirm.claude/pending-reviews/<sha>.mdappears🤖 Generated with Claude Code