Skip to content

Harden claude_code judge (system-priority prompt, no tools) + fix path sanitizer blocking all edits#8

Merged
dburks-svg merged 3 commits into
mainfrom
bench/claude-code-provider-security
Jun 26, 2026
Merged

Harden claude_code judge (system-priority prompt, no tools) + fix path sanitizer blocking all edits#8
dburks-svg merged 3 commits into
mainfrom
bench/claude-code-provider-security

Conversation

@dburks-svg

Copy link
Copy Markdown
Contributor

What

Addresses both Codex P1 review comments on #6, and fixes a critical latent bug that the dogfooding surfaced along the way.

The Codex P1s (both verified live)

1. System-priority prompt (#6 comment 1). The claude_code judge previously folded the stage system prompt into the same stdin text as the untrusted diff, so a prompt-injection diff could override the role/schema instructions and earn an unearned PASS. The judge now writes the system prompt to a temp file and loads it via --system-prompt-file, keeping it at SYSTEM priority over the diff (which stays on stdin), without the multi-line-argv truncation cmd.exe inflicts on --system-prompt for .cmd shims. Verified live: an in-diff "ignore your instructions" injection is rejected (verdict stays PASS, not INJECTED).

2. No tools for the judge (#6 comment 2). The judge ran with only --disallowedTools Write Edit, but it executes with BENCH_SUBPROCESS=1 (Bench's own hook is bypassed), so an injected diff could drive it to mutate files via other tools. It now runs with --tools "" (no built-in tools) and --strict-mcp-config (no MCP servers). The audit caught that --tools "" alone leaves MCP tools available; --bare would isolate further but strips the subscription auth, so it is not usable.

Critical bug found while fixing the above

utils/diff.py:_normalize_path rejected every absolute path as traversal. Since Write/Edit always pass absolute paths, this made the Oracle veto/garble every edit the moment a real provider was active (the fail-open had masked it). Now resolves against the project root and allows in-root paths (project-relative, nameable for governance), blocking only genuine escapes; the same fix is mirrored in the hook's inline fallback. Drops the os.path.isabs anti-pattern.

This one fix was applied via a direct write (a one-time, approved break-glass) because the broken sanitizer was vetoing its own repair. Every other change in this PR was governed by Bench itself and passed — and Bench correctly vetoed two intermediate attempts (a docstring describing hardening before the code implemented it, and a catch block that deferred its log, a C-001 violation), both corrected.

Tests

288 pass under python -m unittest discover -s tests. New coverage: in-root absolute paths (the core fix had zero coverage — a revert would have stayed green), hook-fallback in-root/escape, multi-turn stdin-absence, --strict-mcp-config presence, and temp-write OSError -> _ProviderError.

Closes the review feedback on #6.

🤖 Generated with Claude Code

dburks-svg and others added 2 commits June 25, 2026 23:43
…ction

Two coupled fixes to the claude_code provider path; the second addresses both
Codex P1 review comments on #6.

utils/diff.py (CRITICAL): _normalize_path rejected EVERY absolute path as
traversal, replacing it with the [PATH_TRAVERSAL_BLOCKED] sentinel. Since
Write/Edit always pass absolute paths, this made the Oracle veto (or fail to
verify) every edit the moment a real provider was active -- the fail-open had
masked it all session. Now resolves the path against the project root and
allows in-root paths (returned project-relative, nameable for governance),
blocking only genuine escapes. Drops the os.path.isabs anti-pattern.

  NOTE: applied via a direct write (bypassing the PreToolUse hook), because the
  broken sanitizer was vetoing its own repair. One-time break-glass, approved,
  to restore governance. Every subsequent edit in this change was governed by
  Bench normally -- and it correctly vetoed a docstring that described the
  hardening before the code implemented it.

utils/api.py (Codex P1 x2): the claude_code judge now (1) loads the stage
system prompt from a temp file via --system-prompt-file so it keeps SYSTEM
priority over the untrusted diff on stdin -- a prompt-injection diff can no
longer override the role/schema instructions -- instead of folding it into the
same stdin text; and (2) runs with --tools "" (no tools) rather than only
--disallowedTools Write/Edit, so an injected diff cannot drive the judge to
mutate files (the child bypasses Bench's own hook via BENCH_SUBPROCESS=1). The
temp file is ephemeral model input, removed in a finally block.

Tests: test_api_claude_cli updated for the new behavior, plus a test proving
the system prompt reaches the file (not stdin) and is cleaned up. 93 tests
pass. Verified live on the subscription: a multi-line Oracle prompt rejected an
in-diff injection attempt (verdict stayed PASS, not INJECTED).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial audit (6 confirmed findings of 10) follow-ups:

- injection (P? -> real): the judge now also passes --strict-mcp-config so it
  has NO MCP tools, not just no built-ins. --tools "" alone drops only built-in
  tools; MCP-namespaced tools and the inherited settings allow-list survived it,
  so "no tools" was overstated. (--bare would isolate further but strips the
  subscription auth -> "Not logged in", so it is unusable here.) Docstring
  updated to match.
- temp-file orphan: record the temp path before f.write so a write/flush
  failure still cleans up the file (and unlink it inline on that error path).
- hook fallback (hooks/pre-tool-use.py): the inline fallback used when
  utils.diff fails to import still blocked every absolute path, reintroducing
  the garble-every-edit bug in degraded mode. Now mirrors _normalize_path's
  resolve-against-root containment, with C-001-compliant inline logging on each
  blocked branch (Bench vetoed the first attempt for deferring the log).
- tests: the core allow-in-root-absolute behavior had zero coverage (a revert
  would have stayed green) -- added _normalize_path and build_diff_info
  regression tests, plus hook-fallback in-root/escape tests, a multi-turn
  stdin-absence assertion, a --strict-mcp-config assertion, and a temp-write
  OSError -> _ProviderError test. 288 tests pass.

Every fix here was governed by Bench itself and passed (it correctly vetoed two
intermediate attempts -- a docstring ahead of its code, and a C-001 deferred
log -- both corrected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3a97a55742

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread utils/diff.py Outdated
Codex P2 on #8: _normalize_path used os.path.realpath(os.getcwd()) as the
project root. When the hook runs with a CWD below the repo root (e.g. a
subdir), an in-repo absolute path resolves to ../... and is wrongly blocked as
traversal, making a legitimate governed edit unnameable in the ledger. Now
derive the root from this module's location (utils/diff.py -> repo root) via a
_PROJECT_ROOT constant, mirroring the hook's existing _REPO_ROOT pattern; the
inline hook fallback uses _REPO_ROOT for the same reason. CWD-independent.

Tests: in-root absolute tests now key off _PROJECT_ROOT, plus a regression test
that chdir's into a subdir and asserts an in-repo absolute path still resolves.
289 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dburks-svg
dburks-svg merged commit e576488 into main Jun 26, 2026
3 checks passed
@dburks-svg
dburks-svg deleted the bench/claude-code-provider-security branch June 26, 2026 08:07
dburks-svg added a commit that referenced this pull request Jul 12, 2026
…ider docs

- CLAUDE.md: python -m bench never existed (no bench package); the real
  entry point is python -m cli. Structure diagram now shows
  .claude/settings.json and the previously undocumented utils/viewer.py,
  utils/stats.py, and viewer CLI command.
- README.md: Quick Start cp path corrected to .claude/settings.json; the
  self-governance receipt is ledger entry #13 (hook fallback-coverage
  veto), not #8 (Oracle constitution-replacement veto).
- utils/api.py: module docstring now documents all three providers,
  including claude_code.
- cli/commands.py: docstring counts all five commands.
- .gitignore: replaced the stock Node/JS template with a lean Python set;
  no tracked files affected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant