Guardrail review (Phase 1)¶
Date: 2026-10-10
Reviewers: (1) an independent general-purpose subagent that had not seen the build session, asked only to break the guardrails in a temporary clone; (2) the engineering lead’s engineering:code-review pass over the full Phase 1 diff.
Scope: .claude/hooks/*.py, .claude/settings.json, reviewer subagent frontmatter, scripts/check_guardrails.py.
Code reviewed: branch phase-01-foundations at d812b40. guard.py was unchanged up to d3063e6. Fixes landed in 6cdb589.
Method (adversarial reviewer)¶
The reviewer cloned the repository into a temporary folder. In the clone it created a prereg-v1 tag, committed tests/golden/expected.csv and docs/prereg/addendum-v1.md, and created untracked data/ and results/ files. It then fed about 150 crafted hook-JSON payloads to the clone’s guard.py with the real venv interpreter (CLAUDE_PROJECT_DIR = clone). No attack command was executed; each was only passed as stdin data. The real repository was read-only, and the temp folder was removed afterwards.
Findings and status¶
ID |
Source |
Finding (attack) |
Severity |
Status |
Evidence |
|---|---|---|---|---|---|
R-NL |
adversarial |
|
critical |
Fixed |
Reviewer mode now rejects |
R-WITH |
adversarial |
|
critical |
Fixed |
Only |
R-PLUG |
adversarial |
|
high |
Fixed |
Pytest options whitelisted to test selection ( |
G-GLOB |
adversarial |
Shell writes via |
medium |
Fixed (variables remain residual) |
Quotes stripped before matching; glob and brace patterns matched with |
D-UIX |
adversarial |
|
medium |
Fixed |
|
C-COMMIT |
adversarial |
|
low |
Fixed |
Commit pathspecs checked (message options skipped); |
S-KEY |
adversarial |
Read deny missed |
low |
Fixed (partly residual) |
Added |
T-UNK |
adversarial |
An unknown edit tool name ( |
info |
Fixed |
Any tool whose name contains Edit/Write and carries |
U-ADD |
adversarial |
|
info |
Accepted (fail-closed by design) |
The real |
CR-1 |
code review |
|
high |
Fixed |
Treated as wrappers, including their value-taking options; |
CR-2 |
code review |
|
high |
Fixed |
Grouping characters stripped from tokens (the |
CR-3 |
code review |
Git aliases ( |
high |
Fixed |
Unknown subcommands resolved through inline |
CR-4 |
code review |
Dynamic command names ( |
medium |
Fixed |
Blocked when the segment touches a protected operation; same tests |
CR-5 |
code review |
Lock tags or refs could be edited directly inside |
high |
Fixed |
Edit/Write inside |
CR-6 |
code review |
|
high |
Fixed |
Settings files other than the shared |
CR-7 |
code review |
PowerShell |
medium |
Fixed |
Network hints (WebClient, DownloadFile, HttpClient, urlretrieve, …) and any Python command carrying a URL are host-checked; |
CR-8 |
code review |
|
medium |
Fixed |
|
CR-9 |
live use |
False positive: |
info |
Fixed |
|
CR-10 |
live use |
False positive: |
info |
Fixed |
|
CR-11 |
live use |
False positive: |
info |
Fixed |
Redirection searched outside quotes only; |
CR-12 |
check_guardrails |
|
low |
Fixed |
Twins added; scenario 4 of |
Residual risks (not fixable in a hook, or accepted) are listed in ADR-0001, “Residual risks”.
Correctly blocked (coverage audit, from the adversarial report)¶
Tool path locks: Edit, Write and NotebookEdit with changed content,
replace_alland unknownold_string; CRLF-identical and byte-identical writes correctly allowed;docs/../docs/PREREG.md,docs//PREREG.md,./docs/PREREG.md,docs\PREREG.md,DOCS/PREREG.MD, padded and absolute paths.Plain shell writes:
sed -i,cp,mv,tee,>/>>,python -c open(w),install,dd,ln -sf,truncate,awk >,patch,: >,cd docs && …, subshells,git checkout/restore/rm/mvof PREREG, PowerShellSet-Content/Add-Content/Out-File; read-only commands allowed.Tags:
git tag -d/-f/--delete/-fd,git update-ref -d refs/tags/prereg-v1,git push origin :refs/tags/prereg-v1; benign tag operations allowed.Pushes:
+main,+refs/…,--force,-f,-uf,--force-with-lease,--force-if-includes,--mirror,--prune,--delete,:main,git -C . push -f,git -c x=y push -f,env/command/"git"/\git//usr/bin/git,bash -c,sh -c,eval, PowerShelliexand-EncodedCommand.Data staging:
git add data/…,-f,results,src data,git stage,--chmod=+x data/…, PowerShell variant;git add -A/./'*'/:/blocked when.gitignoreis removed, allowed when data is ignored.Recursive deletes:
rm -rf src,rm -r --no-preserve-root /,rm -rf .venv,rm -rf $HOME,find -delete,find -exec rm -rf {} +,git clean -fdx, Pythonshutil.rmtree/os.removedirs,xargs rm -rf,Remove-Item -Recurse,rd /s /q,ri -r; cache deletes allowed.Downloads: unlisted hosts, lookalikes (
pypi.org.evil.com,evil.com/pypi.org,user@pypi.org@evil.com),169.254.169.254, uppercase schemes,$URL, scheme-less hosts,certutil,bitsadmin,scp,pip/uv install https://evil, Python urllib,iwr/irm.Input handling: malformed, empty, 200 KB, non-UTF-8, wrong-type, deeply nested, 500-pipe and 5,000-label-URL inputs all returned 0 or 2, never 1 (no fail-open crash).
Live-guard observations¶
The active guard blocked several of the reviewer’s own commands, each for a stated reason:
writing into the real repo’s
data/(permission deny);git addof not-yet-existing paths (dry-run fail-closed);git -C "$C" add …(unresolvable variable).
Reading a dummy .env and id_rsa in the scratch folder succeeded before the S-KEY fix.
Verification after fixes (commit 6cdb589)¶
uv run pytest -q: 258 passed, including 52 regression tests for the findings above.uv run python scripts/check_guardrails.py: 45/45 scenarios passed; guard latency median about 188 ms..claude/settings.jsonvalidates against https://json.schemastore.org/claude-code-settings.json.