Skip to content

Judge an edit as the file it would leave, not as its fragment - #168

Merged
jothimani-rajendran merged 2 commits into
mainfrom
claude/gate-edits-whole-file
Sep 27, 2026
Merged

jothimani-rajendran merged 2 commits into
mainfrom
claude/gate-edits-whole-file

Conversation

@jothimani-rajendran

@jothimani-rajendran jothimani-rajendran commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

What

The first real Claude Code run of the java-security agent kit (Windows) showed Claude change OrderRepository.java with Edit, adding "... WHERE customer = '" + customer + "'". PreToolUse allowed it; only the Stop hook refused it afterwards.

Cause: an Edit/MultiEdit payload carries only the text replaced and the text put in, and the write gate judged that fragment alone. It has no imports, class or neighbours, so a whole-file policy (java-security's script gate) found nothing in it. Claude's exact edit gets no finding as a fragment, and persistence-sql-string-concat at line 38 as the file it leaves.

Fix, part 1: edits (gate/edit_image.py, stdlib, bundled into every runtime).

  • The gate now judges the file the edit would leave: the file on disk with the call's replacements applied, in order.
  • It covers replace_all, MultiEdit edits[], CRLF files, and the old_string/new_string, oldString/newString and old_str/new_str spellings.
  • An edit that can't be rebuilt is judged as its fragment, as before.

Fix, part 2: Codex apply_patch (gate/patch_image.py, stdlib, bundled).

  • Codex writes through apply_patch, whose only argument is the patch text in tool_input.command.
  • Each file the patch adds or updates is rebuilt from disk and judged whole. The rebuild follows Codex's documented patch format:
    • hunks in order, with @@ anchors;
    • *** End of File and *** Move to;
    • tolerance for trailing spaces.
  • A hunk that can't be placed leaves that file to the turn's end.
  • Scope: Codex's hooks still call the gate only at Stop; chock emits no PreToolUse write fragment for Codex. This part is the runtime half, and wiring is a separate change.

Unchanged behaviour: added_lines still means only the introduced text. The payload carries it as added, which an older runner ignores.

Proof against the real policy, using Claude's actual Edit payload in an agent-kit workspace:

Where the edit arrives This branch Released 0.11.3
Installed .chock/bin/claude_code.py (repo route) deny allow
A java-security 0.4.3 Claude plugin built from this branch, in a plain repo with no chock (plugin route) deny allow

Codex's apply_patch payload through .chock/bin/codex_cli.py against the real gate: deny on this branch, allow on 0.11.3.

Definition of done

  • chock check → 0 errors, 0 warnings, 0 infos
  • chock check --only matrix passes; no surface or claim changes
  • chock sync --repo . --check clean (vendored runtimes regenerated)
  • chock check --only verify clean
  • Registry rescanned; no stale entries
  • pytest: new tests/test_write_gate_edits.py (24) and tests/test_write_gate_patch.py (29). edit_image and patch_image are at 100% line and branch coverage.
    • The core tests fail on main.
    • End to end through the real runner, a staged_blob gate sees the whole file, while an added_lines gate sees only the edit or patch additions.
    • One test fails only in this sandbox, whose system python3 lacks jsonschema: test_validate_hook_interpreter. It passes in CI.
  • pytest acceptance/ …: 21 passed
  • Existing artifacts migrated: runtime goldens and .chock/bin/* regenerated
  • Touched manifests: none; changelog Unreleased entries added
  • ruff check . and ruff format --check . clean

Claims

  • No surface is described as enforcing more than it installs. The Codex change is stated as runtime-only, and no coverage claim changes.

Found while doing this (not fixed here): Gemini CLI's pre-tool write fragment (gemini_cli-write-hooks.json) is emitted but never installed. in_agent_generic merges only <vendor>-hooks.json, so .gemini/settings.json gets the Stop hook alone.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CzNYfzP8ymU3r4JB9Sz8Ha

Claude Code changes an existing file with Edit or MultiEdit, whose
payload carries only the text replaced and the text put in. The write
gate judged that fragment alone -- no imports, no class, no neighbours --
so a policy that reads whole files found nothing and the edit went
through; only the Stop hook refused it afterwards. The first real Claude
Code run of the java-security agent kit showed exactly that.

An edit is now judged as the file on disk with the call's replacements
applied in order (replace_all honoured, CRLF files matched), for Claude
Code's spelling and the oldString/newString and old_str/new_str ones.
A kind reading added lines still judges only the introduced text: the
payload carries it as `added`, which an older runner ignores. When the
file cannot be rebuilt the fragment is judged as before. The edit
helpers live in gate/edit_image.py, bundled beside write_gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
@jothimani-rajendran
jothimani-rajendran marked this pull request as ready for review September 26, 2026 21:40
Codex CLI writes through apply_patch, whose only argument is the patch in
tool_input.command -- no path, no content -- so the write gate had nothing
to judge. gate/patch_image.py (stdlib, bundled beside edit_image) parses
Codex's documented patch format and rebuilds each added or updated file
from disk: hunks in order, @@ anchors, *** End of File, *** Move to,
trailing-space tolerance. A hunk that cannot be placed leaves that file to
the turn's end. added_lines sees only the + lines. Through the vendored
codex runtime, a patch adding concatenated SQL is now denied by the real
java-security gate (0.11.3: allowed).

Codex's hooks still call the gate only at Stop; wiring PreToolUse for its
write tool is a separate change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
@jothimani-rajendran
jothimani-rajendran merged commit 8f0a730 into main Sep 27, 2026
19 checks passed
@jothimani-rajendran jothimani-rajendran mentioned this pull request Sep 27, 2026
11 tasks
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.

2 participants