Fix Claude Code Hook Installation and Review Launch

I fixed the finding on trail 1300 (PR #2364), pushed the fix as 9def7c3fe3 and resolved the finding. It's one file, +31/−17.
The finding was real. The test that makes every place in the package that starts claude have a recorded isolation policy only looked for "claude" passed directly as an argument to a call. GenerateText stores it in a variable first (claudePath := "claude"), so the test never saw it.
What changed:
- The test now counts any
"claude"string inside a function, however it's used. - It now fails on a package-level
"claude"constant, because it can't tell which function that belongs to. - I added
GenerateTextto the policy list as isolated: it runs with no setting sources and from a temp directory.
I checked it both ways: the test passes with the change, and fails on GenerateText (generate.go:160) when its policy entry is removed. The package's tests and mise run fmt && mise run lint pass locally.
Still open:
- CI: the checks on the new head, including Entire Gates, are still pending.
- New findings: the review hasn't added any yet, but it may not have re-run on this push. I'll check again once the gates finish.
- Older findings: six stale findings from earlier heads are still open, including a High on
review_launch.go:74about user hooks. I haven't looked at whether they still apply. If stale findings still count toward the findings gate, that High one could keep it red.
The branch is checked out in a scratch worktree at scratchpad/rci, since there wasn't a worktree for it before.
Both findings on trail 1367 are accurate against the current head, but I haven't changed anything. Victor pushed 6 commits to peyton/checkpoint-remote-surface today, the latest being e74523ac85, and these two findings came from the review of that push. He may already be working on them, and editing his branch in parallel risks a conflict.
1. 01M3CHKVZWFE (Low): status names a checkpoint store a fork contributor never reaches.
- The case: someone clones the upstream repo and adds a
forkremote. - Before their first push: the elected sync remote is still origin. Origin by itself passes the ownership check, so
statussays checkpoints sync to the dedicatedentireio/cli-checkpointsstore. - On the first
git push fork: Entire switches the sync remote tofork, the ownership check fails, and checkpoints go to the fork instead. - Is it wrong?
statusis correct about the current election. What it doesn't do is predict the switch that the first push will cause. From then on, both commands report it correctly. - To fix it:
statuswould have to guess the next switch from the branch's configured push destination.CLAUDE.mddeliberately doesn't let that configuration pick the remote, because doing so caused a regression before. - Recommendation: dismiss it as by-design, or record it as an accepted divergence in a comment.
2. 01M3CHKTN72W (Low): enable stays silent where status warns.
- The case: pushing is turned off (
push_sessions=false) and checkpoint reads are refusing the store for a reason other than ownership. - What happens:
statusreports the configured store as ignored ("checkpoint reads do not resolve to it").reportIgnoredCheckpointRemote, the functionenableuses to report it, has no equivalent branch, soenableprints nothing. - Is it in scope? The finding says this predates the PR. But the
enablereporter was added in this PR, ande74523ac85was meant to make the two commands agree, so this is a gap that PR left. - Recommendation: fix it with one shared function that both commands use to decide what to report about an ignored store. That's a small change.
Should I go ahead (fix #2, dismiss #1) on top of e74523ac85, or check with Victor first? If I proceed, I'll pull the stale worktree forward before making changes.
Both trail 1367 findings are handled, and the trail now shows no open findings. I pushed the fix to peyton/checkpoint-remote-surface as cf5a961235, on top of Victor's e74523ac85. No new commits had landed on the branch before the push.
01M3CHKTN72W: resolved.- Fix:
statusandenablenow take their ignored-store report from one shared function,ignoredCheckpointRemote. - Wider than the finding: with
push_sessions=false,enableused to exit before looking at the store at all. So it missed ownership refusals too, not only the "reads don't resolve" case. - What
enabledoes now: it checks whether checkpoint reads resolve to the store, the same waystatusdoes. - Claim prompt: it is still only offered when ownership can't be established either way.
- Fix:
01M3CHKVZWFE: dismissed as by-design.statusreports the remote that is elected right now. Predicting the switch the firstgit push forkwill cause would mean letting the branch's push config pick the remote, which the election deliberately doesn't do. The report is correct from the first push onward.
The diff is +126/−29 across 3 files. The new test, TestEnableAndStatusAgreeWithPushingDisabled, runs the real enable command and status and checks they print the same line in two cases: an ownership refusal and a reads-side refusal. It passes with the fix, and both cases fail without it. mise run fmt && mise run lint and the cli and checkpoint/remote package tests pass locally. CI for the new push hasn't finished yet.
On trail 1300, the Entire Gates check is red only because nobody has approved it yet. CI and findings both pass there, and no new findings appeared after my earlier push.
can you address these findngs on https://entire.io/gh/entireio/cli/trails/1367/say-when-a-checkpoint-remote-is-being-ignored-and-how-to
<pasted_content id="2c80"> docs/development/checkpoint-implementation.md:65 now contradicts the code. It still says the pre-push warning names the claim command "whether the owner is disproved (a fork) or merely unprovable", and that it "fires on every push while the condition holds". Both stopped being true in 36596bbde1, and the doc wasn't part of these commits. The PR description is more out of date than before.
Its headline example output is the fork case (origin owner "alice" differs from checkpoint owner "acme"), which no longer prints at push time. It still says "fires on every push", "Three commits", and "Merge this after #2519". The #2519 dependency went away in the commit before this round. </pasted_content id="2c80">
I fixed both: the doc and the PR description (#2521) now match what the code does. The doc fix is pushed as 2f88319f35, and the trail description is updated to match the PR, without the trail's link to itself.
Doc (docs/development/checkpoint-implementation.md:65):
- Warnings on push: Entire now warns during a push only when ownership can't be established either way, and only on a push that actually delivered checkpoints. The doc says why a fork contributor, whose owner doesn't match, gets no warning.
entire statusandentire enable: both commands report both cases, from one shared function, in the same sentence. A fork's mismatch shows as information; an owner that can't be established shows as a warning. Withpush_sessionsoff, they judge where checkpoints are read from instead.
PR description:
- Example output: the push example is now the case that still prints, where the owner can't be determined. The fork example moved to the
status/enablesection, which is where that case now appears. - Removed: "fires on every push", "Three commits", and the "Merge this after #2519" section.
- Push-time claim line: it now says "new checkpoints go there from your next push", matching the current code.
- Added: your
push_sessionsfix, the rule that a mismatching owner outranks one that can't be read, the refusal to write a trackedsettings.local.json, and a note on the fork contributor who hasn't pushed to their fork yet (the finding I dismissed). - Verification section: it now says the tests against a built binary ran on an earlier head, before the push-warning change, and that this round only ran fmt, lint, and the
cliandcheckpoint/remoteunit tests. CI runs the rest.
Trail 1367 still shows no open findings after the push, but the review may not have re-run on the new head yet.
The new trail 1367 finding (01M3DKYHD6VX) was a false positive. I dismissed it, and pushed a test that pins the case it described as 0380261400.
Why there's no gap: the finding says that if every remote has an owner that can't be read, the ownership check falls through and treats the store as ours. It doesn't. Each remote the check skips for that reason records the "owner could not be determined" reason before moving on. So the check can only answer "ours" when every remote was read and its owner matched. If none can be read, the answer is "can't be established".
The test: TestCheckpointRemoteIsInherited_NoReadableOwnerIsUnprovable runs that case with only origin, with origin plus other remotes, and with no origin at all. When I removed the line that records the reason, all three failed. Lint and the checkpoint/remote package tests pass.
Trail 1367 shows no open findings again. The review may not have re-run on the new head yet.
I fixed both findings on trail 1300 (PR #2364), pushed them as two commits, and resolved the findings. The two commits are +155/−14 in total.
01M3EJEBCHG8: the reviewer's PATH could point into the reviewed checkout (20cbb1dd23). Before, the directory of the runningentirebinary was always put first on the reviewer's PATH. Whenentireis built into the repo root, as this repo's own build tasks do, that directory is the checkout itself. The prepend is now skipped when it's inside the reviewed checkout.- Wider than the finding: any other absolute PATH entry inside the checkout (a
<repo>/bin, for example) is dropped too, since it holds branch content in the same way. - Bug the new test caught: macOS temp paths go through a
/var → /private/varsymlink. A PATH entry that didn't exist yet was compared without resolving it, so it looked like it was outside the checkout. It's now resolved through its nearest existing parent directory. - Trade-off: if the only
entireon PATH is the one inside the checkout, the review hooks can't find it and the review goes unrecorded. The alternative is running a binary from the branch being reviewed.
- Wider than the finding: any other absolute PATH entry inside the checkout (a
01M3EJC3WEHY: the installer could write an empty hook (990f0cd985). An empty command is now never written into the user's.claude/settings.json. The new testTestInstallHookEntries_InstallsFullHookInventorychecks that every hook in the inventory gets installed with a real command. That gives back, at CI time, the check the compiler used to provide. I also corrected the comment that claimed a missed lookup did nothing.
I reverted each fix in turn (the prepend, the in-checkout entries, a mismatched hook lookup, and the missing empty-command check), and each revert failed its test. Lint and the claudecode and review package tests pass. Trail 1300 has no open findings for now; the review may not have re-run on the new head yet.
I fixed the new trail 1300 finding (01M3N86Z3RXK, High), pushed it as b847242e0b, and resolved it. My earlier fix (990f0cd985) skipped empty commands only for the tool-use hooks. The other hooks in the inventory (SessionStart, Stop and the rest) could still be written with an empty command, so the "the installer skips it" comment was only half true. That was a one-line guard. Lint and the claudecode package tests pass.
I didn't add a test for it. Those hooks take their commands straight from the inventory list, so this case only arises if someone writes an empty command into that list. The inventory test from last round already checks every installed hook has a real command.
Trail 1367 has no open findings.
On trail 1300 the risk score rose from 28 to 54 at b847242e0b, but that looks like scoring swing, not something the commit caused. Security went from 0 to 15 on the same push.
Why it's not the change:
- Size:
b847242e0bis a one-line guard that stops the installer from writing an empty hook command. That can't account for a 26-point rise. - History: on the same trail, risk has read 54, 45, 28 and now 54 again. The first 54 was the head before any of my fixes, so it's back where it started.
- Rationale:
- At 54, the reviewer points to the hook installer reaching every user on upgrade. That part has been in the PR from the start.
- At 28, the one-line summary didn't mention it.
- Both readings list the same mitigating factors: nothing destructive in git, fail-closed, bounded to review capture.
- Security score: the security rationale at 15 finds no new weakness ("purely defensive hardening"). At 0 it said essentially the same thing.
For comparison, trail 1367 swung 22 → 35 → 58 → 42 across its last four heads with small changes too.
I couldn't check whether the higher risk score changes the Entire Gates result, because GitHub rate-limited the request. The last time I checked, the only failing gate was Approvals.
Trail 1300 has no risk gate. I read the gates from Entire's own API rather than GitHub, and there are four: Approvals, Checks, Findings and Up-to-date. The risk score is a monitor on the trail page, not a gate, so going from 28 to 54 doesn't block merging.
| Gate | Blocking | State at b847242e0b |
|---|---|---|
| Approvals | yes | failed: 0 of 1 required approvals, and self-approval isn't allowed |
| Checks | yes | pending: CI still running on the new head |
| Findings | yes | passed: no open findings, and the review covered this head |
| Up-to-date | no | skipped |
The only thing actually blocking is the missing approval from someone other than you. Checks should flip to passed once CI finishes.
On the rate limit: the 403 came from GitHub's REST API for your account, and it hit a gh call I made. Entire wasn't involved. I've switched to Entire's API for gate state, so it doesn't matter here.
The risk prompt is .entire/runners/trail-risk.json on main; this branch doesn't change it. The rise comes from which scoring band the model puts the PR's hook-installer change in.
How the prompt scores:
- Whole PR every time: each push re-scores the entire
git diff origin/main...HEAD, currently 13 files, +1770/−35. It never scores just the latest commit, so a one-line change can move the score if the model reads the same diff differently. - The bands that matter here:
- 16–30, Low: "isolated changes, tests, read-only output"
- 31–50, Moderate: "new commands/flags in contained packages; no git-mutating or egress paths"
- 51–70, Elevated: "git hooks, checkpoint/session internals, auth/token handling, new dependencies"
- The factor it cites: "Blast radius — … installed hooks … reach every user on upgrade."
Why this PR moves between bands: it has two parts.
- The review launch isolation, which on its own reads as contained (16–30 or 31–50).
- A refactor of
hooks.go(+68/−16). That's the code that writes Entire's session-capture hooks into every user's.claude/settings.jsonwhen they runentire enable.
When the model notices the second part, it applies "checkpoint/session internals" and "installed hooks reach every user", which lands in 51–70. The 54 rationale says exactly that: "lifecycle-hook installer (session-capture internals reaching every user on upgrade)". The 28 rationale mentions only "config isolation hardening", so that pass effectively scored the review half alone.
My last two commits both changed the installer, and their messages name the user's .claude/settings.json. That probably made the hooks.go change easier for the model to spot. The bigger reason is that, under the prompt as written, 51–70 is the more defensible reading: the installer change really does reach every user. So 28 was the outlier, not 54.
To make this stable, the prompt would need to say whether Claude Code's session-capture hooks count as "git hooks / session internals". Its wording only names git hooks (prepare-commit-msg, post-commit, …). Editing the prompt changes how every trail in the repo is scored, so it's your call rather than something to change inside this PR.
I had only read the last two rationales; I've now read all six, and I got one thing wrong. I said the 28 run scored only the review half of the PR. It didn't: it also names "refactors hook installation". Every run names the same main risk and the same mitigations, but the score doesn't follow from them.
| Head | Score | Main risk named | Mitigations named |
|---|---|---|---|
5aafca9c89 | 44 | hooks.go installer refactor "reaches every user on upgrade" | set unchanged, drift-tested, fails closed |
978fc40505 | 44 | "the one user-reaching touch" is the hook-inventory refactor | guarded, behaviour-preserving |
357df6b6cd | 54 | lifecycle-hook installer, "runs for every user on upgrade" | no destructive git ops, fails closed |
9def7c3fe3 | 45 | "modifies git hook installation" | fails closed, extensive tests |
990f0cd985 | 28 | "refactors hook installation" | extensive tests |
b847242e0b | 54 | lifecycle-hook installer, "session-capture internals reaching every user" | no destructive ops, fails closed, contained |
What that shows:
- Consistent reading, different numbers. Every run finds the one thing that puts this PR in the 51–70 band, the
hooks.goinstaller refactor. Each then decides differently how far "fails closed, tested" pulls it back down, and the results range from 28 to 54. - Not caused by recent commits. 54 appeared before I touched the branch (
357df6b6cd). None of my commits added risk. Two of them were guards that stop the installer writing an empty hook, which, if anything, make the installer safer. - The one thing that would move it: the installer refactor itself. As long as this PR changes the code that writes hooks into every user's
.claude/settings.json, the model will keep landing near the 51–70 boundary.
If you want the score lower and steadier, the fix is scope: split the hooks.go inventory refactor into its own small PR, or keep the installer's inline hook commands and give the review launch its own copy, with the existing drift test keeping the two in sync. Otherwise nothing needs to change; risk isn't a gate on this trail.
It isn't a fix, and it isn't really separate from trail 1300. I was wrong to suggest splitting it off as if it were independent.
What the hooks.go change is: a refactor with no change in behaviour, +84/−35 across 5 files.
- Before: the eight Claude hook commands Entire installs were written inline inside
installHookEntries. - After: they live in one list,
entireHookSpecs(), and the installer reads from it. Five hooks are read directly; the three tool-use hooks are looked up by type and matcher (specCommand). - What users get: the same eight commands in
.claude/settings.json.TestInstallHookEntries_InstallsFullHookInventorypins that. - The empty-command guards (my last two fixes) can't trigger with the current list. They only protect against a future bad edit to it.
Why it belongs to 1300: the refactor only exists because of the review isolation.
- The PR launches the reviewer with
--setting-sources "", so Claude loads nothing from the reviewed checkout, including its.claude/settings.json. That file is controlled by the branch being reviewed. - That file is also where Entire's own session-capture hooks live. Without them, the review runs but records no session.
- So the review launch writes its own settings file (
buildTrustedReviewSettings) and passes it with--settings, carrying exactly the hooks the installer writes. - Two places now need the same eight hooks, and the refactor makes the installer and the review launch read one list so they can't drift apart.
That leaves two real options:
- Keep it as is. It's the natural cost of the isolation. The risk monitor will keep scoring near 51–70 because the diff touches the installer, even though the installer's output is unchanged.
- Leave the installer untouched and give the review launch its own copy of the eight hooks, with a test that fails if the two lists differ. The installer diff goes to zero, but you get duplicated hook definitions guarded only by a test, which is exactly the drift the refactor was written to prevent. That trades real design quality for a steadier score on a monitor that isn't a gate, so I'd keep it as is.
One actual defect while I'm in there: the new type was inserted between installHookEntries's doc comment and the function. The comment starting "…set, any current Entire hook) first. Returns the number of hooks newly added…" now attaches to entireHookSpec, and installHookEntries has no doc comment. It's a two-line move; want me to push it?
No. The review launch can get its hook list by calling main's existing installer, installHookEntries, and hooks.go doesn't need to change at all.
On main, installHookEntries(rawHooks, force) only transforms a map: it reads hook entries from rawHooks, adds Entire's eight, and writes them back. It touches no files, runs nothing, and reads no settings. So the review launch can build its trusted settings file like this:
What that gets you:
hooks.goidentical to main. The whole +84/−35 installer diff goes away, includingentireHookSpec,specCommand, the empty-command guards, and the misplaced doc comment. That also removes what keeps the risk monitor near 51–70.- Always the same hooks as
entire enable. The review file is the installer's own output, so there's no second list to keep in sync. That's stronger than the refactor, which still needed thespecCommandlookup to be right. - Simpler validation.
validateTrustedReviewSettingsbecomes: at least one hook, and no empty commands. It no longer needs the spec list.
What it costs: the tests that loop over entireHookSpecs() (the "full inventory" ones in review_launch_test.go and the installer test I added) get rewritten to compare the review file against a fresh install. It's roughly the same amount of test code, no new behaviour, and the PR shrinks.
One thing to confirm when I implement it: the force parameter only changes how existing entries are cleaned up, and the map starts empty here. I'll check that, then run the tests and the mutation checks as before. Shall I go ahead on review-config-isolation?
Yes. It keeps the PR to what it claims to be, a fix to how reviews launch, and it's the better design anyway.
- Scope:
hooks.gogoes back to exactly main.entire enableand every user's.claude/settings.jsonaren't touched at all by a PR about review. - Correctness: the review's hooks are the installer's own output, so they can't drift from what
entire enablewrites. The refactor only made drift less likely. - Less code:
entireHookSpec,specCommand, three rounds of empty-command guards, and the misplaced doc comment all go away. The two trail findings about them (01M3EJC3WEHY,01M3N86Z3RXK) disappear with the code. - Risk score: with the installer out of the diff, the thing the risk monitor keeps landing on is gone.
The change sits in review_launch.go and its tests: buildTrustedReviewSettings calls installHookEntries on an empty map, validateTrustedReviewSettings checks for at least one hook and no empty commands, and the inventory tests compare the review file against a fresh install. I'll add it as a normal commit on review-config-isolation, without rewriting history, then run lint, the tests and the mutation checks.
Want me to go ahead?
hooks.go is back to exactly what's on main, pushed to review-config-isolation as e311fde38a. PR #2364 now touches 11 files (+1714/−19), down from 13 files (+1770/−35), and no longer touches hooks.go or hooks_test.go at all.
- Where the reviewer's hooks come from:
buildTrustedReviewSettingsruns the installer's own function (installHookEntries) against an empty document and uses what it writes. So the review gets exactly the hooksentire enableinstalls, with no second list to keep in sync. - Removed: the shared hook list, the lookup that could return an empty command, all three rounds of empty-command guards, the installer test I added, and the misplaced doc comment. The two trail findings about them (
01M3EJC3WEHY,01M3N86Z3RXK) no longer apply. - Checks before launch: the review still refuses to start unless its settings carry every hook the installer writes, each with a non-empty command.
- The main test:
TestTrustedReviewSettings_CarriesFullHookInventorynow installs hooks for real into a.claude/settings.jsonon disk and checks the review's hooks are identical to what was written.
I broke the code on purpose twice to confirm the tests catch it. Dropping the Stop hook from the review settings fails the inventory test and the check-before-launch test. Removing the missing-hook check fails the "one hook type dropped" case. Lint and the claudecode and review package tests pass.
The PR description said one hook list was shared by the installer and the review launch. I changed that line to describe the new approach and copied the description to the trail. docs/architecture/review-command.md had the same claim and is updated in the commit.
The branch now lives in a worktree at .worktrees/review-config-isolation, because the scratch copy was cleaned out of /private/tmp.
Trail 1300 shows no open findings. The review may not have re-run on the new head yet, so the risk score on this head isn't in either.
under risk again says "could block reviews if validation is overly strict." - are we being too strit on validation, or lets walk through validation too
The launch validation isn't too strict; it can't fail in normal use. There are two places where reviews can be blocked or weakened, though, and neither is that check.
These are all the gates between entire review and Claude starting:
| # | Gate | When it fails | Verdict |
|---|---|---|---|
| 1 | validateTrustedReviewSettings: every hook the installer writes is in the review's settings, each with a command | Only through a coding mistake. The review's hooks now come from the installer itself, so this passes whenever entire enable works | Not too strict. It exists because a review without these hooks runs but records nothing |
| 2 | Writing the 0600 settings file to the temp dir | Temp dir not writable | Fine. Nothing works in that state anyway |
| 3 | Built-in skills such as /review | Never. They're passed through untouched | Fine |
| 4 | Skill staging: a configured skill that discovery can't find | The profile names a skill that discovery doesn't list | Can block legitimate reviews. See below |
| 5 | Skill with an unexpected layout (file not under skills/, commands/ or agents/) | Only if discovery returned such a path | Hard to reach, since discovery only scans those three directories |
| 6 | Skill name containing /, \ or .. | Only for a malformed name | Correct: the name becomes a path Entire writes to |
Gate 4 in detail. Discovery (DiscoverReviewSkills) only looks in the plugin cache and ~/.claude/{skills,commands,agents}, one level deep. So these fail:
- A project skill, such as
.claude/commands/review.mdin the repo. Before this PR, Claude loaded project settings and the skill worked. Now the review fails with "/reviewis not installed", which is wrong: it is installed, just in a place the isolated reviewer won't load from. Refusing is right, because the branch being reviewed controls that file and could rewrite the review instructions. The message needs fixing. - A nested command, such as
~/.claude/commands/team/review.md(invoked as/team:review). That's the user's own file, and Claude supports this layout, but discovery never finds it. This is over-strict, a real false block.
Profiles built with the skill picker can't hit either case, because the picker uses the same discovery. Hand-written profiles can.
Two things that aren't validation but can make reviews fail or get weaker:
- Users who authenticate only through
apiKeyHelper. The helper isn't carried into the review, because it would run inside the reviewed checkout. Entire warns before launch, but the review then fails authentication unlessANTHROPIC_API_KEYis set or they're signed in to Claude. That's deliberate and loud. - No permission allowlist. The reviewer runs with
--permission-mode defaultand loads no settings, so there's nopermissions.allowlist. In-pmode, tool calls that would normally prompt are denied rather than asked. If the reviewer needsBashforgit difforgit log, those calls may now be refused. The review still "succeeds", but it's weaker, which is worse than a clean failure. An older finding (01M27HXYAZZT) raised the same point. I haven't verified what the reviewer actually tries to run; I'd check this first, because nothing reports it.
What I'd do:
- Make discovery scan command subdirectories, to fix the nested-command false block.
- Change the gate-4 message for skills that exist in the project: "project-level skills aren't loaded for reviews, because the reviewed branch controls them; move it to
~/.claude/commands". - Run a real
entire reviewand check whether the reviewer'sgitcalls get denied. If they do, give the reviewer an explicit, read-only allowlist in its own settings file, rather than loading the user's.
Want me to take these on, starting with #3 since it's the one nobody would notice?
It isn't too Claude-specific; that part is fine. But ~/.claude/commands is the wrong directory to point people at.
Claude-only is correct here. The message would come from review_skills.go in the claudecode package, which runs only when the reviewer is Claude. Each agent resolves skills its own way (Codex, Gemini and the rest have their own review adapters), and the launch-guard test already scopes this isolation to the Claude package. A Claude path in a Claude error is accurate. It would only be wrong in shared review code, and it won't live there.
But commands/ is the wrong suggestion. Claude now recommends skills (~/.claude/skills/<name>/SKILL.md); commands/ is the older, flat-file format. Pointing people at it steers them to the legacy layout.
Better: list the locations discovery actually scans, and nothing else. Discovery looks in the plugin cache plus ~/.claude/skills, ~/.claude/commands and ~/.claude/agents. The message should say something like:
/reviewisn't available to the isolated reviewer: project-level skills aren't loaded for reviews, because the branch under review controls them. Install it at user level (~/.claude/skills/review/SKILL.md) or as a plugin.
To keep it from going stale, I'd build the location list from the same list DiscoverReviewSkills scans, rather than hard-coding it in the message. Then if discovery gains nested-command support (fix #1) or a new location, the message follows automatically.
Should I fold that into the three fixes? The order would be: the permission check first, then nested commands, then this message.
yes but again you have claude specific skills, commands, and agents file still and users with review command, select multiple agents
You're right. That message was Claude-specific at the wrong layer, and I also got two things wrong earlier. The skill checks users see already work per agent, and this PR doesn't add new false blocks for skills.
What already handles multiple agents on main:
- Every agent gets the same skill check before it starts.
VerifyConfiguredSkillsInstalled(review/picker.go) checks each configured skill against that agent's own discovery (Claude, Codex, Antigravity) using one generic message. - In a multi-agent review, one worker's missing skill doesn't stop the others.
review/cmd.go:1251printsskipping reviewer <worker>: …and the rest still run. - A worker that fails at launch doesn't stop the others either, including a failure in Claude's skill staging.
run_multi.go:149records it as that worker's failure and cancels only that worker.
Corrections to what I said before:
- Project skills and nested commands were already rejected before this PR. Claude's discovery never scanned either, so the check above refused them on main too. My earlier "this used to work" was wrong.
- Claude's staging check isn't what users hit. It runs after the shared check, so a skill discovery doesn't list fails first with the shared message. Claude's version is only reached in edge cases.
Revised plan:
- The permission check: still in scope. Can the isolated Claude reviewer run the
gitcommands it needs without any allowlist loaded? That's the one thing this PR could make silently worse. - The Claude-specific message: dropped. The shared message is where users look, and it already names the worker in multi-agent runs.
- Follow-ups, not this PR (they predate it):
- The shared message doesn't say where each agent looks for skills. Each agent's discovery could return its own search locations, so every agent's line lists its own paths rather than hard-coded Claude ones.
- Claude's discovery doesn't find nested commands (
~/.claude/commands/team/review.md). - Antigravity's discovery does scan project-level
.agents/skillsin the reviewed checkout, so an Antigravity reviewer uses skills the branch controls, while the Claude reviewer no longer does. That's the same isolation question, for another agent.
Shall I start on #1?
Every agent gets the same skill check before it starts - users configure which skills they want theri agent to run adn can change from agent to agent. like claude and codex have different skills, or like even their review prompt from agents is not the exact same
Right. "The same skill check" was misleading. The function is shared, but what it checks is per agent: each worker's own configured skills against that agent's own discovery. The code confirms this is per worker all the way through:
- Config: each worker reads its own entry,
profile.Agents[workerName], so Claude's skills and Codex's skills are separate lists (review/cmd.go:1244). - Check:
VerifyConfiguredSkillsInstalled(ctx, ag, agentCfg)runs once per worker. Claude's skills are checked against Claude's discovery and Codex's against Codex's, and a failure skips only that worker. - Prompt: each worker's prompt is composed from its own config: the shared task plus that agent's own skills and always-prompt (
review/types/reviewer.go:95). So the prompts differ between agents, as you said.
What that means for this PR: everything it adds applies to the Claude worker's configuration only.
- Skill staging: only the Claude worker's skills are copied, from the
RunConfigbuilt for that worker. Codex's skills and prompt are untouched. - Isolation flags and PATH cleanup: they apply only to the
claudeprocess. - Mixed reviews are only partly isolated. In a Claude + Codex review, the Claude worker ignores the checkout's configuration while the Codex worker still loads its own from that checkout. The PR says it's scoped to the Claude package, but a user picking several agents wouldn't know one of them is isolated and the other isn't.
That last point belongs with the follow-ups: either give the other adapters the same treatment, or say which agents run isolated in the review output and docs. It doesn't change #1, the permission check, which concerns only what the isolated Claude worker can run. Shall I start on that?
Yes, but not as one mechanism added to this PR. Each agent loads its checkout's configuration differently, so each needs its own fix, and I'd do them as separate PRs. There are three review adapters: Claude (this PR), Codex and Pi.
Pi: the most exposed, and the easiest to fix.
- Exposure: Pi automatically loads project-local extensions from
.pi/extensions/in the reviewed checkout. Those are TypeScript files, and Pi runs them, so a branch can ship code that executes as soon as its review starts. Today'spi --mode json --print <prompt>launch does nothing to prevent that. It's the same hazard this PR closes for Claude, and more direct. - Fix: Pi's CLI has the flags to mirror Claude's approach:
--no-extensions, plus-e <path>to load Entire's own capture extension from a copy Entire controls rather than from the checkout.--no-skillsplus--skill <path>for the worker's configured skills, the same staging idea as Claude.--no-prompt-templates.- The PATH cleanup can be shared as-is.
Codex: partly protected already; needs research before changing anything.
- Existing protection: Codex has its own trust system. Project hooks don't run without persisted trust, and I'm told the project config layer is trust-gated too.
- What I don't know: whether that trust is tied to the repo or to each hook's content. If it's tied to the repo, a reviewed branch's
.codex/hooks.jsonruns in any repo the user has trusted, which includes every repo where Entire installed Codex hooks. - The flags aren't a clean fit:
--ignore-user-configexists,-coverrides individual values, and there's an option to skip execpolicy rules. I didn't find a flag to skip the project layer. So the Codex fix depends on how its trust actually works, which I'd check with a real branch before designing anything.
What can be shared across all three:
- The PATH cleanup.
- The prepare step that sets things up before launch and cleans up afterwards (
PrepareCmd). - The launch-guard test, extended so each package's direct
codex/pilaunches need a recorded policy too.
Suggested order: land this PR as the Claude step, then a Pi PR (clear hazard, clear flags), then Codex once its trust behaviour is verified. This PR's description should also say plainly that only the Claude worker runs isolated, so users running mixed reviews aren't misled in the meantime.
Want me to add that line to the description and start on the Pi PR from a fresh branch off main? The permission check for the Claude reviewer is still queued behind it.
does this, https://entire.io/gh/entireio/cli/trails/1449/agent-give-summary-generators-no-tools-and-an-empty-working/changes, have any relation with shared helper/worker
Yes. Trail 1449 is the same kind of fix as 1300, for summaries instead of reviews, and the two branches collide. I trial-merged them in a throwaway worktree (since removed).
1. Same threat, different path.
- 1449 covers summary generation: the prompt carries untrusted transcript content. It removes the agent's tools (
--tools ""for Claude) and runs from a fresh empty directory instead of the shared temp dir. It does that for every generator agent: Claude, Codex, Copilot and Cursor, with Antigravity getting only the empty directory. - 1300 covers the review worker, where the untrusted input is the checkout's configuration. It's Claude only, and it can't remove tools, because the reviewer needs them.
2. They break each other when merged, both as a text conflict and as a test failure.
- Text conflict: both edit the
constblock inclaudecode/generate.go. Both addflagSettingSources, so that part resolves easily. - Test failure: 1449 moves the
"claude"string into a package-level constant,claudeBinary. With 1449's side resolved, 1300's launch-guard test fails three ways:
The rule I added last week, rejecting a package-level "claude" string, was a blunt workaround. The right fix is for the guard to treat any use of a constant whose value is "claude" as a launch site. Then claudeBinary is allowed and still tracked, and GenerateText and GenerateTextStreaming are found through it. The policy note for them should also say "empty working directory" rather than "temp cwd". Whichever PR lands second has to carry that fix. Since 1300 owns the guard, I'd make the guard change on 1300 now, so it works with or without 1449.
3. 1449 is the template for isolating the other review agents.
- Per-agent pattern: it already does isolation one agent at a time, using each CLI's own flags. That's the shape a Pi or Codex review fix would take.
- Reusable helpers:
agent.RejectedFlagandUnsupportedFlagError: if Pi or Codex is too old for an isolation flag (--no-extensions, say), the review fails with "update pi" instead of silently running without isolation.NewTextGenerationDir: reusable in principle, though a reviewer has to run inside the checkout, so an empty working directory doesn't apply there.
Want me to change the guard on 1300 to follow "claude" constants, so it's ready for 1449 whichever merges first?
We didn't settle that, and I should have been clearer. When you asked whether we could cover all agents, I said yes but recommended separate PRs. I then asked whether to start on Pi, and that question was never answered before we moved to 1449. So nothing has been started for Pi or Codex.
If you want one fix covering every review agent, I'll do it. The remaining choice is where it goes:
- Extend PR #2364 (trail 1300) to Pi and Codex. One PR delivers "reviews don't trust the checkout's config, whatever agent you pick". The downside is a bigger diff.
- Separate PRs, one for Pi and one for Codex, on top of this one. Each is smaller to review, but mixed-agent reviews stay only partly protected until all three land.
Either way the work is the same:
- Pi: switch off discovery of extensions, skills and prompt templates. Load Entire's capture extension, and the worker's configured skills, explicitly from copies Entire controls, and apply the shared PATH cleanup.
- Codex: first check with a real branch whether Codex's hook trust is tied to the repo or to each hook's content. Then choose flags or overrides based on what that shows.
- Shared: the launch guard covers
codexandpilaunches too, and handles constants such as 1449'sclaudeBinary. If a CLI is too old for an isolation flag, the review fails with "update the CLI", reusing 1449'sRejectedFlagandUnsupportedFlagError.
Which do you want?
Yes. PR #2364 (trail 1300) is Claude-only right now, and so is everything I've pushed to it this week.
- Isolated today: the Claude review worker only. It loads no settings from the reviewed checkout, gets Entire's hooks from its own settings file, stages its configured skills into a directory Entire controls, and runs with the cleaned PATH.
- Unchanged: the Codex and Pi review workers. They launch exactly as on main and still load the checkout's configuration. For Pi that includes running project-local TypeScript extensions from
.pi/extensions/. - The guard test: it covers only direct
claudelaunches in theclaudecodepackage.
So a review with Claude alone is protected, while a Claude + Codex or Claude + Pi review is only partly protected. The PR description doesn't currently say that, which is misleading for multi-agent users whichever way we go next.
I couldn't find a ticket for PR #2364.
- The PR itself: no linked closing issue. The description and all 16 commit messages mention no Linear ID, GitHub issue or advisory. The branch is
review-config-isolation, not apeyton/ent-…branch generated from a Linear issue. - Linear: searches for "review", "reviewer configuration" and "untrusted checkout" return nothing about isolating the review worker's configuration. The nearest ones are unrelated: ENT-2035 (
entire reviewhangs on a Claude judge), ENT-2220 (prompt-injection audit of entire-api LLM calls), and ENT-298 (transcript rendering). - The two GHSA IDs (GHSA-hqjp and GHSA-2h46) that the security score cited aren't from this PR. They appear only in
.entire/runners/trail-security.json, the security reviewer's own prompt, as past advisories to check changes against.
The PR was opened on 2026-09-11 under your account (peyton-alt), and the description frames it as a hardening fix. If it came from a private security report, that would be a draft GitHub advisory, which I can't see from here: GitHub's API is rate-limiting your account right now. You'd know better than the repo whether there's a ticket behind it.
It matters for the scope question. If a ticket or advisory named Claude specifically, a Claude-only PR is correct as filed and Pi and Codex are follow-ups. If it reported "entire review trusts the checkout's configuration" in general, the other agents belong in it too.
No, there's no plain ticket behind it either. PR #2364 has no linked issue, and its description, commit messages and branch name (review-config-isolation) mention no ticket. Linear has no issue for isolating the reviewer's configuration. I couldn't search GitHub issues, because the API is still rate-limiting your account.
It looks like it was opened directly as a hardening fix. I can file a Linear ticket for the all-agents scope, with the Claude step as this PR and Pi and Codex as the remaining work, if you want something to track it against.
You were right: it's Plain ticket T-243, "Security Report: entire review --target can execute remote branch-controlled commands before the first model request". Chetan Reddy reported it on 2026-09-09. It's assigned to you, with Stefan added, and labelled security. PR #2364 was opened two days later and isn't linked to it anywhere, which is why I missed it.
What the report covers:
- Claude only, as reported. A branch under review commits a
.claude/settings.jsonwith aSessionStarthook.claude -ploads it without a trust prompt, and the hook runs as the reviewing user before the first model request. Chetan reproduced it 4/4 times on Windows, with Entire0.10.7-nightlyand Claude Code 2.1.237. - The fix he asked for: stop loading the branch's execution-capable Claude config (hooks, MCP, plugins, commands, skills), reintroduce only the lifecycle hooks Entire needs, and don't read the branch's settings file just to recover them. That's what #2364 does.
- Also requested: a regression test proving a committed
SessionStartcanary does not run, for both current-branch review and--target. #2364 has unit tests on the launch arguments and the settings file, but no end-to-end canary test.
The mismatch you need to know about: on 2026-09-29, your reply to Chetan from security@entire.io said the fix "also covers Pi and Codex". It doesn't. PR #2364 is Claude-only, and Pi and Codex reviewers still load the checkout's configuration. For Pi that includes running .pi/extensions/*.ts.
That settles the scope question: to match what the reporter was told, Pi and Codex belong in this fix. Either extend #2364, or tell Chetan it ships in stages. I won't contact him; that's your call.
Given T-243, I'd extend #2364 in this order:
- Pi, the clearest hazard, with clear flags to fix it.
- The canary end-to-end test the report asked for, running a committed
SessionStarthook through both current-branch review and--targetfor each agent. - Codex, once its trust behaviour is verified.
Then link T-243 from the PR description. Want me to start?
i mean this, thttps://entire.io/gh/entireio/cli/trails/1432/review-don-t-load-the-reviewed-checkout-s-agent-config-into/changes
i mean this, thttps://entire.io/gh/entireio/cli/trails/1432/review-don-t-load-the-reviewed-checkout-s-agent-config-into/changes i think in the ticket.
Yes, that's the ticket's fix. Trail 1432 is PR #2598, "review: don't load the reviewed checkout's agent config into reviewers", which you opened on 2026-09-25. It covers Claude, Codex and Pi, so it's what your T-243 reply meant by "also covers Pi and Codex". PR #2364 (trail 1300) is a separate, older, Claude-only fix for the same problem. The two overlap and will conflict.
| #2598 (trail 1432) | #2364 (trail 1300) | |
|---|---|---|
| Opened | 2026-09-25 | 2026-09-11 |
| Size | 11 files, +384/−13 | 11 files, +1714/−19 |
| Agents | Claude, Codex, Pi | Claude only |
| Claude settings | --setting-sources user: drops project and local settings, keeps the user's own | --setting-sources "": loads nothing |
| Entire's hooks | --settings inline JSON, from installHookEntries on an empty map | a 0600 file, built the same way (my change yesterday) |
| MCP | --strict-mcp-config | --strict-mcp-config |
User hooks and apiKeyHelper | still run, with the checkout as the working directory | not loaded; the helper isn't run, with a warning |
| Skills | work normally, since user settings still load | staged into a directory Entire controls |
| PATH cleanup, pinned permission mode, reviewer system prompt, launch guard | no | yes |
| Codex | on --target runs, marks the checkout untrusted with a -c override, so Codex's whole project layer is off | not covered |
| Pi | --no-approve --no-extensions, plus Entire's extension loaded from a copy Entire writes | not covered |
How they relate:
- #2598 fixes T-243 as reported. Branch-controlled project config no longer runs for any of the three agents, and it's the one that matches what you told Chetan.
- #2364 is stricter for Claude. It also closes a gap #2598 leaves open: with
--setting-sources user, the user's own hooks andapiKeyHelperstill run with the reviewed checkout as their working directory. A user hook likenpm testormakewould then run branch code. That's a second-order hole, beyond what T-243 reported. - They collide on:
buildReviewCmdinclaudecode/reviewer.go, which each rewrites.- The
flagSettingSourcesconstant ingenerate.go. - Two different pre-launch hooks added to the review launch in
review/types/template.go(Preparein #2598,PrepareCmdin #2364). - The same installer-derived hook settings, built twice.
Recommendation: land #2598 first as the T-243 fix. Then rebase #2364 onto it as a Claude-only hardening follow-up:
- Changes: move Claude from
userto"", with skills staging, PATH cleanup, the permission pin and the guard test. - Drops its duplicates: its own hook-settings builder and its own
PrepareCmd, in favour of #2598'sPrepare.
That shrinks #2364 considerably and keeps the reporter-facing fix small and done. One thing to check first: the end-to-end canary test T-243 asked for (a committed SessionStart hook must not run) isn't in either PR.
Want me to check #2598's trail findings and CI first, or start rebasing #2364 onto it?
Yes, and that matters for #2598. This repo commits agent config for all three agents, so reviewing it (or any repo set up the same way) changes what the reviewer gets.
| Committed in this repo | What it holds | Under #2598 |
|---|---|---|
.claude/settings.json | Entire's hooks, plus enabledPlugins and extraKnownMarketplaces for the repo's local plugins (e2e, agent-integration) | Dropped. The hooks come back via --settings; the plugins don't |
.claude/skills/, commands/, agents/ | /changelog, /test-repo, /dev, /reviewer, … | Probably not loaded (see below) |
CLAUDE.md | Repo rules, including review guidance ("Reviewers should flag any new git.PlainOpen…") | Probably not loaded |
.pi/extensions/entire/index.ts | Entire's Pi capture extension | Dropped by --no-approve --no-extensions, replaced by a copy Entire writes. Fine |
.codex/config.toml, .codex/hooks.json | Entire's Codex hooks and config | Kept on a plain review; switched off only on --target |
Three consequences:
-
Plain review loses the repo's own setup for Claude and Pi. With
--setting-sources user, Claude skips project settings. Most likely it also skips projectCLAUDE.md, skills, commands and agents: in the Agent SDK those need theprojectsource. I'd confirm that with a real run before relying on it. If so:- A review profile that uses a project skill such as
/reviewerstops working. - The reviewer no longer sees the repo's review rules.
That's a quality regression, not a safety one.
- A review profile that uses a project skill such as
-
The agents don't behave the same way. Codex isolates only on
--target, "a branch someone else may control", and keeps normal trust on a plain review. Claude and Pi isolate on every review, including the user's own checkout. The same repo gets reviewed differently depending on the agent. -
T-243 asked for both cases. The report wants the canary blocked for current-branch review too, and that's a fair ask:
gh pr checkout 123 && entire reviewmakes the current branch someone else's code. So "isolate only on--target" doesn't fully satisfy the report either.
A way to square it: isolate on every review, as #2598 does for Claude and Pi, and bring the Codex plain-review path in line. Then put back the non-executable parts of the repo's config explicitly: CLAUDE.md and AGENTS.md as prompt context, and the profile's configured skills staged the way #2364 does. The executable parts (hooks, MCP, plugins, extensions, apiKeyHelper) stay out. The reviewer keeps the repo's guidance and the user's chosen skills, and nothing from the branch runs.
Want me to confirm with a real claude -p --setting-sources user run whether project CLAUDE.md and skills load, before deciding?
Confirmed: with --setting-sources user, which #2598 uses, the reviewer gets none of the repo's own Claude setup, including CLAUDE.md. I tested with Claude Code 2.1.286 (Haiku) in a throwaway repo containing a CLAUDE.md with a secret word, a project skill, a project command, and a project SessionStart hook.
| Mode | CLAUDE.md in context | Project command /probecmd | Project skill /probeskill | Project hook ran |
|---|---|---|---|---|
| default (no flag) | yes, 3/3 | loaded | loaded | yes |
--setting-sources user (#2598) | no, 0/3 | "not available" | "not available" | no |
--setting-sources "" (#2364) | no, 0/3 | "not available" | "not available" | no |
The CLAUDE.md column comes from runs with tools switched off (--tools ""). In my first pass, "" seemed to load CLAUDE.md, but the model had simply read the file with its Read tool. So a reviewer with tools can still read CLAUDE.md if it thinks to. It just isn't given the file up front.
What this means for #2598:
-
The fix works. The branch's
SessionStarthook didn't run underuser, which is exactly what T-243 reported. -
Reviewing a repo like this one loses:
CLAUDE.mdas context, and with it the repo's review rules.- Every project skill, command and agent:
/reviewer,/changelog,/test-repoand the rest. - The project plugins switched on in
.claude/settings.json.
A review profile that names a project skill gets "not available" and reviews nothing useful.
-
userstill loads the user's own skills and plugins (the listing showedtest-auditor,superpowers:*,codex:*).""loads only Claude's built-in skills.useralso keeps the user's own hooks andapiKeyHelper, which then run with the reviewed checkout as their working directory. That's the extra gap #2364 closes.
So the direction from before holds. Keep the executable config out, and put back only the non-executable parts explicitly:
CLAUDE.mdandAGENTS.mdas prompt context, marked as coming from the branch under review.- The profile's configured skills, staged from a copy Entire controls.
Nothing I tested loads CLAUDE.md without also loading project settings, so it has to be put back explicitly.
I removed nothing: the probe repo is in the scratchpad (ss-probe/). Claude also recorded these probe sessions under ~/.claude/projects/-private-tmp-…-ss-probe-repo/, which you can delete.
Next is making #2598 the T-243 fix and shipping it, then folding #2364 in behind it. You need to make one call first.
The decision: should #2598 ship as-is, or should it restore CLAUDE.md and project skills first?
- Ship as-is. It closes the reported hole for all three agents now, and it's what Chetan was told. The cost: reviews temporarily lose
CLAUDE.mdcontext and project skills.reviewis an experimental command, so that's tolerable for a short window. - Restore first. No regression, but the reporter's fix waits on more work: putting back
CLAUDE.mdandAGENTS.mdas prompt context, plus skill staging.
I'd ship as-is, and add the canary test first.
Plan:
- #2598, before merging
- Add the regression test T-243 asked for: a committed
SessionStart(Claude) hook, and the equivalent for Pi and Codex, must not run for either current-branch review or--target. - Check its trail findings and CI.
- Link T-243 in the description, and note the known context loss.
- Add the regression test T-243 asked for: a committed
- Follow-up, #2364 rebased onto #2598
- Claude goes from
userto"", so the user's own hooks andapiKeyHelperno longer run in the checkout. - Put back
CLAUDE.mdandAGENTS.mdas context marked "from the branch under review". - Stage the profile's skills, for Claude and for Pi via
--skill. - Keep the PATH cleanup and the launch guard, updated for #2598's
Preparestep and for #1449'sclaudeBinaryconstant. - Drop #2364's duplicate hook builder and
PrepareCmd.
- Claude goes from
- Before step 2: verify whether Pi's
--no-approvealso dropsAGENTS.md/SYSTEM.md, and whether the isolated Claude reviewer'sgitcalls get denied without a permission allowlist. Both change what the follow-up has to restore. - Reporter: tell Chetan once #2598 is released. That message is yours to send.
Shall I start step 1 on #2598?
Stopped here, because the newest commit on #2598 rests on a claim my test contradicts. On Claude Code 2.1.286, the reviewer's flags stop Claude from registering any of the checkout's commands or skills. That is what init reports, straight from Claude:
| Mode | Checkout commands registered | Checkout skills | Result of the branch's shadow /review |
|---|---|---|---|
| default (no flags) | probecmd, probeskill, review | probeskill | Loaded. Its ! line was attempted (blocked by a permission check), and the SessionStart hook ran |
#2598's reviewer flags (--setting-sources user --settings … --strict-mcp-config) | none | none | Claude's built-in /review ran instead. No marker written |
What that means for #2598:
- Finding
01M3SWP54QS9says checkout commands still resolve under--setting-sources user, so a branch can shadow/reviewand run!lines. On 2.1.286 that's false. I'd dismiss it with the evidence above. - Commit
b4df75ad35("stop a--targetreview when the branch changes Claude commands") says "Verified: under the reviewer's flags, Claude discovers the checkout's.claude/commands". My run doesn't reproduce that. On this version, the check blocks reviews of any branch touching.claude/commands,.claude/skillsor.claude/agents, unless the user passes--trust-target-commands. It protects against something that doesn't happen here. It might still matter on an older Claude Code where--setting-sourcesdidn't govern command discovery, but I haven't checked that.
The decision:
- Drop
b4df75ad35if 2.1.286 behaviour is what we support. Simpler, and no false blocks for branches that legitimately edit.claude/, which includes this repo's own. - Keep it as protection for older Claude Code versions, with a version check that skips it on versions that behave like this one. That needs a test on an older version first, which I can run with
npx @anthropic-ai/claude-code@<version>.
Then I continue with the canary regression test on #2598. Which way on b4df75ad35?
what do you think? we should support all claude versions, but can claude version that old be run?
Drop b4df75ad35. It guards against something no version of Claude Code does, and old versions do still run, so I could check. Every version I tried installs from npm (528 are published, back to 0.2.9) and runs against the API today.
I ran #2598's reviewer flags in the probe repo, with a project command, skill, shadow /review and SessionStart hook:
| Claude Code | Accepts the flags | Checkout commands and skills loaded | Branch's hook ran |
|---|---|---|---|
| 2.1.286 (current) | yes | none | no |
| 2.1.237 (the reporter's version) | yes | none | no |
| 2.1.100 | yes | none | no |
| 2.0.50 | yes | none (the review listed is Claude's built-in: it also appears in an empty directory) | no |
| 2.0.0 | yes | none (same) | no |
| 1.0.100 | no: error: unknown option '--setting-sources' | nothing starts | no |
As a control, 2.0.x without the flags does load the checkout's probecmd and probeskill. So the probe works on old versions, and the flags are what switch discovery off.
Why dropping it is right for all versions:
- Versions with
--setting-sources(2.0.0 onward, at least) never load checkout commands under these flags. The command check would only ever block legitimate branches that edit.claude/, which includes this repo's own. - Versions without it reject the flag and exit before loading anything, so they fail closed. They're safe; the only problem is a confusing error.
What to do instead for old versions:
- Revert
b4df75ad35, which includes the--trust-target-commandsflag. - When Claude rejects
--setting-sources,--settingsor--strict-mcp-config, fail the review with "Claude Code is too old for isolated reviews; update it" instead of the raw error. #1449 addsagent.RejectedFlagandUnsupportedFlagErrorfor exactly this. Until #1449 lands I'd make the check a small local one, to avoid depending on it. - Dismiss finding
01M3SWP54QS9with this table as evidence. - Record the tested version range in
review-command.md.
The old versions are installed under scratchpad/cc-versions/, so the canary test can use them. Shall I make those changes on #2598, then add the canary test?
The changes are pushed to #2598 (trail 1432), and the finding is dismissed. Trail 1432 shows no open findings, though the review may not have re-run on the new head yet. The PR is now +598/−28 across 11 files.
245dd5d030: reverts the command-change check (b4df75ad35). That removes the--trust-target-commandsflag and stops--targetreviews being blocked for branches that edit.claude/.1bc9336c11: clear failure for old Claude Code.- The review launcher gets an optional
ClassifyExitstep. - The Claude reviewer uses it: when Claude rejects
--setting-sources,--settingsor--strict-mcp-config, the review fails with a message to update Claude Code (2.0.0 or later) instead of a bareunknown option. - Two new tests cover it. Removing either piece of the change fails its test.
- The error pattern matches what Claude Code 1.0.100 actually prints.
- The review launcher gets an optional
- Docs (
review-command.md) and PR description:- Record that the flags keep the checkout's commands, skills and agents out, with the versions tested (2.0.0 through 2.1.286).
- Correct the line claiming
CLAUDE.mdand skills are still read:CLAUDE.mdisn't preloaded, and project skills don't resolve. - Replace the command-check rows with the version results.
- The trail description is synced.
- Finding
01M3SWP54QS9: dismissed, citing the version test.
Lint is clean, and the claudecode, codex, pi and review package tests pass. I didn't run the integration or e2e suites this round.
Still to do on #2598 is the regression test T-243 asked for: a committed SessionStart hook and the Pi and Codex equivalents must not run, for both current-branch review and --target. After that, #2364 goes on top of it.
Yes. The report asked for one, and today's coverage only checks the arguments Entire passes. Nothing proves the branch's hook actually doesn't run, and that's the part that silently breaks if Claude Code, Pi or Codex changes how their flags behave. The version table I built by hand is exactly that check.
How I'd build it, at near-zero cost:
-
Real agent CLIs. Real
claude,piandcodexagainst a throwaway repo, through the realentire reviewcommand, both on the current branch and with--target <remote-only branch>. -
A deliberately bad API key. The branch's hook, MCP server or extension fires when the agent starts, before any model request, which is how the reporter reproduced it on an unauthenticated machine. So the run fails authentication after the moment that matters, and no tokens are spent.
-
One marker per canary, written outside the worktree:
Agent Canary the branch commits Claude SessionStarthookClaude .mcp.jsonserverPi .pi/extensions/canaryCodex .codex/config.tomlMCP serverThe test asserts each marker is absent. A control run with the isolation flags removed must write every marker, so the test can't pass just because the canary never worked.
-
Where it lives. Behind the
e2ebuild tag, skipped when the agent CLI isn't installed. It needs real binaries, andCLAUDE.mdkeeps real-agent tests out of the default suite. The Vogon canary can't stand in: it's a fake agent and doesn't load any of this config. -
Optional: a version matrix using the old Claude Code installs in the scratchpad (2.0.0 → 2.1.286, plus 1.0.100's "update Claude Code" error). I'd run it once and record the result in the PR rather than make CI do it.
Two caveats:
- The new tests only run where the CLIs are installed. That's the e2e runners and developer machines, not every CI job.
- For the control run I'd have the test call the agent directly with the same argv minus the isolation flags, rather than adding a test-only switch to Entire.
Shall I add it to #2598?
Base directory for this skill: /Users/peytonmontei/.claude/plugins/cache/superpowers-marketplace/superpowers/5.0.2/skills/test-driven-development
Test-Driven Development (TDD)
Overview
Write the test first. Watch it fail. Write minimal code to pass.
Core principle: If you didn't watch the test fail, you don't know if it tests the right thing.
Violating the letter of the rules is violating the spirit of the rules.
When to Use
Always:
- New features
- Bug fixes
- Refactoring
- Behavior changes
Exceptions (ask your human partner):
- Throwaway prototypes
- Generated code
- Configuration files
Thinking "skip TDD just this once"? Stop. That's rationalization.
The Iron Law
Write code before the test? Delete it. Start over.
No exceptions:
- Don't keep it as "reference"
- Don't "adapt" it while writing tests
- Don't look at it
- Delete means delete
Implement fresh from tests. Period.
Red-Green-Refactor
RED - Write Failing Test
Write one minimal test showing what should happen.
<Good> ```typescript test('retries failed operations 3 times', async () => { let attempts = 0; const operation = () => { attempts++; if (attempts < 3) throw new Error('fail'); return 'success'; };const result = await retryOperation(operation);
expect(result).toBe('success'); expect(attempts).toBe(3); });
Vague name, tests mock not code </Bad>
Requirements:
- One behavior
- Clear name
- Real code (no mocks unless unavoidable)
Verify RED - Watch It Fail
MANDATORY. Never skip.
Confirm:
- Test fails (not errors)
- Failure message is expected
- Fails because feature missing (not typos)
Test passes? You're testing existing behavior. Fix test.
Test errors? Fix error, re-run until it fails correctly.
GREEN - Minimal Code
Write simplest code to pass the test.
<Good> ```typescript async function retryOperation<T>(fn: () => Promise<T>): Promise<T> { for (let i = 0; i < 3; i++) { try { return await fn(); } catch (e) { if (i === 2) throw e; } } throw new Error('unreachable'); } ``` Just enough to pass </Good> <Bad> ```typescript async function retryOperation<T>( fn: () => Promise<T>, options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; onRetry?: (attempt: number) => void; } ): Promise<T> { // YAGNI } ``` Over-engineered </Bad>Don't add features, refactor other code, or "improve" beyond the test.
Verify GREEN - Watch It Pass
MANDATORY.
Confirm:
- Test passes
- Other tests still pass
- Output pristine (no errors, warnings)
Test fails? Fix code, not test.
Other tests fail? Fix now.
REFACTOR - Clean Up
After green only:
- Remove duplication
- Improve names
- Extract helpers
Keep tests green. Don't add behavior.
Repeat
Next failing test for next feature.
Good Tests
| Quality | Good | Bad |
|---|---|---|
| Minimal | One thing. "and" in name? Split it. | test('validates email and domain and whitespace') |
| Clear | Name describes behavior | test('test1') |
| Shows intent | Demonstrates desired API | Obscures what code should do |
Why Order Matters
"I'll write tests after to verify it works"
Tests written after code pass immediately. Passing immediately proves nothing:
- Might test wrong thing
- Might test implementation, not behavior
- Might miss edge cases you forgot
- You never saw it catch the bug
Test-first forces you to see the test fail, proving it actually tests something.
"I already manually tested all the edge cases"
Manual testing is ad-hoc. You think you tested everything but:
- No record of what you tested
- Can't re-run when code changes
- Easy to forget cases under pressure
- "It worked when I tried it" ≠ comprehensive
Automated tests are systematic. They run the same way every time.
"Deleting X hours of work is wasteful"
Sunk cost fallacy. The time is already gone. Your choice now:
- Delete and rewrite with TDD (X more hours, high confidence)
- Keep it and add tests after (30 min, low confidence, likely bugs)
The "waste" is keeping code you can't trust. Working code without real tests is technical debt.
"TDD is dogmatic, being pragmatic means adapting"
TDD IS pragmatic:
- Finds bugs before commit (faster than debugging after)
- Prevents regressions (tests catch breaks immediately)
- Documents behavior (tests show how to use code)
- Enables refactoring (change freely, tests catch breaks)
"Pragmatic" shortcuts = debugging in production = slower.
"Tests after achieve the same goals - it's spirit not ritual"
No. Tests-after answer "What does this do?" Tests-first answer "What should this do?"
Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge cases, not discovered ones.
Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't).
30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work.
Common Rationalizations
| Excuse | Reality |
|---|---|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
| "I'll test after" | Tests passing immediately prove nothing. |
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
| "Existing code has no tests" | You're improving it. Add tests for existing code. |
Red Flags - STOP and Start Over
- Code before test
- Test after implementation
- Test passes immediately
- Can't explain why test failed
- Tests added "later"
- Rationalizing "just this once"
- "I already manually tested it"
- "Tests after achieve the same purpose"
- "It's about spirit not ritual"
- "Keep as reference" or "adapt existing code"
- "Already spent X hours, deleting is wasteful"
- "TDD is dogmatic, I'm being pragmatic"
- "This is different because..."
All of these mean: Delete code. Start over with TDD.
Example: Bug Fix
Bug: Empty email accepted
RED
Verify RED
GREEN
Verify GREEN
REFACTOR Extract validation for multiple fields if needed.
Verification Checklist
Before marking work complete:
- Every new function/method has a test
- Watched each test fail before implementing
- Each test failed for expected reason (feature missing, not typo)
- Wrote minimal code to pass each test
- All tests pass
- Output pristine (no errors, warnings)
- Tests use real code (mocks only if unavoidable)
- Edge cases and errors covered
Can't check all boxes? You skipped TDD. Start over.
When Stuck
| Problem | Solution |
|---|---|
| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. |
| Test too complicated | Design too complicated. Simplify interface. |
| Must mock everything | Code too coupled. Use dependency injection. |
| Test setup huge | Extract helpers. Still complex? Simplify design. |
Debugging Integration
Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression.
Never fix bugs without a test.
Testing Anti-Patterns
When adding mocks or test utilities, read @testing-anti-patterns.md to avoid common pitfalls:
- Testing mock behavior instead of real behavior
- Adding test-only methods to production classes
- Mocking without understanding dependencies
Final Rule
No exceptions without your human partner's permission.
I've paused before pushing or starting the Codex canary. Another session committed 8e55412d69 ("review: set codex cmd.Dir only on --target runs") into this same worktree at 15:44, between my commits. It isn't pushed, and it changes the Codex reviewer, which is exactly what the Codex canary would test and possibly edit.
Done locally, not pushed (the remote head is still 1bc9336c11):
| Commit | What | Checked |
|---|---|---|
7f7468a104 | Claude canary: the branch's SessionStart hook must not run, for current-branch review and --target | Fails with main's reviewer (hook ran in both cases), passes on #2598 |
794a09441b | Pi fix: Pi 0.70.2 doesn't just warn on --no-approve, it exits 1, so every Pi review there failed with a bare option error. It now says to update Pi | Unit test written first and seen failing |
c48184e6c1 | Pi canary: the branch's .pi/extensions must not load | Fails with main's reviewer, passes with #2598 on Pi 0.87.1 and 0.99.2. On 0.70.2 the review fails closed with the new message |
How the canaries work:
- They drive the real CLIs through
entire review. - Each run is set up so no model request can happen: Claude gets an invalid API key; Pi gets a bogus model, plus a private config dir with no login.
- Each asserts two things: the agent was launched, and it got past loading config. So a run that never got there can't pass by accident.
- They're skipped unless
ENTIRE_TEST_REVIEW_REAL_AGENTS=1is set.
Things I found along the way that need correcting:
- The PR description is wrong about Pi. It says older Pi only warns on
--no-approve, and it names Pi 0.87.1 without saying where it comes from. Pi moved to@earendil-works/pi-coding-agent(now 0.99.2). The old@mariozechnerpackage tops out at 0.73.1, and no release there has--no-approve. - Your installed
piis 0.70.2 from the old package, so with #2598 your own Pi reviews will now fail with "update pi". - Newer Pi loads project extensions only in repos the user has trusted. The attack needs a trusted repo, which the Pi test now sets up.
Codex is the remaining canary, and the description needs the Pi corrections. Should I include 8e55412d69 when I push, and go ahead with Codex? Or is that other session still working on the Codex reviewer, in which case I'd wait for it?
Yes. It was the herdr session in the support worktree (.worktrees/support), working in this worktree from there.
- Claude session:
784bf4a7-68f9-4468-bf42-3af0d3f9827f, from the commit'sEntire-Checkpoint: 01M3T7VF2CECP7AERZ1RST4J4Jtrailer. Its transcript is under~/.claude/projects/-Users-…-worktrees-support/, and nearly every command it ran started in.worktrees/support. It made the commit in.worktrees/review-isolation, which is #2598's branch, the same worktree I'm using. - herdr: workspace
wB(no custom name, working directory.worktrees/support), first tab, public pane 1. The pane has no name, so that location is the best identifier I can give you. - What it was doing: its recent prompts are "anither finding on 1422", "fix the cmd.Dir finding", "have you tested in new biary", and "can you give me the trail link" (answered with trail 1449). So it's fixing trail findings on the Codex reviewer. Its transcript was last written at 16:06, after my commits at 16:01, so it may still be active.
- Not the peer called
support-b2: that's the onlysupportsession in my peer list, but it started 5 days ago and I couldn't match it to this session ID, so I wouldn't assume it's the one.
Since it's editing the Codex reviewer in the same worktree, I'd leave Codex to that session, or wait until it's finished. Want me to push my Claude and Pi commits now, including its 8e55412d69 since it's underneath them, or hold everything until you've checked with that session?
These are the four trails this session has worked on, plus the security report behind two of them:
| Trail | PR / branch | What it is | Where it stands |
|---|---|---|---|
| 1432 | #2598, fix/review-isolate-agent-config | The fix for T-243. Reviewers stop loading the reviewed checkout's agent config, for Claude, Codex and Pi | Remote head 1bc9336c11. I have 3 local, unpushed commits: the Claude canary, the Pi fail-closed fix, and the Pi canary. Another session's 8e55412d69 (Codex) sits underneath them. 1 new open finding: 01M3W79BWN1Y (Medium, Codex reviewer: falls back to "" when json.Marshal(root) fails). It's in the Codex code the other session is working on |
| 1300 | #2364, review-config-isolation | Stricter, Claude-only hardening of the reviewer, kept separate from #2598 | My last push was e311fde38a, which takes the hooks from the installer and leaves hooks.go alone. 0 open findings. Next: rebase it on #2598 as the follow-up ("" instead of user, putting CLAUDE.md/AGENTS.md back as context, skill staging) |
| 1449 | fix/summary-no-tools (PR #2619 per the other session) | Summary generators get no tools and an empty working directory. Same kind of fix as 1300, for summaries | Not changed in this session; I only compared it. It will break 1300's launch-guard test (its claudeBinary constant) whichever merges second. 0 open findings |
| 1367 | #2521, peyton/checkpoint-remote-surface | Say when a checkpoint_remote is being ignored, and how to claim it | Merged. Findings fixed or dismissed earlier this session (cf5a961235, 2f88319f35, 0380261400) |
The security report behind 1432 and 1300 is Plain T-243, "entire review --target can execute remote branch-controlled commands", from Chetan Reddy, assigned to you. Your 2026-09-29 reply told him the fix "also covers Pi and Codex", which matches #2598.
Only 1367 has been merged. The other three are still open and in review.
| Trail | Status | Title |
|---|---|---|
| https://entire.io/gh/entireio/cli/trails/1367 | merged | Say when a checkpoint_remote is being ignored, and how to claim it (#2521) |
| https://entire.io/gh/entireio/cli/trails/1432 | open, reviewing | review: don't load the reviewed checkout's agent config into reviewers (#2598, the T-243 fix) |
| https://entire.io/gh/entireio/cli/trails/1300 | open, reviewing | fix(review): scope reviewer configuration to trusted sources (#2364) |
| https://entire.io/gh/entireio/cli/trails/1449 | open, reviewing | agent: give summary generators no tools and an empty working directory |
Only trail 1432 has an open finding; 1300 and 1449 have none.
1432: 01M3W79BWN1Y (Medium), cmd/entire/cli/agent/codex/reviewer.go:111-112.
- The issue: when
json.Marshal(root)fails,untrustedProjectOverridefalls back to""and producesprojects={""={trust_level="untrusted"}}. That marks an empty path untrusted instead of the checkout, so on a--targetrun the override would silently match nothing. - Why it's real: it contradicts the file's own fail-closed rule a few lines down: "Any failure is returned; a guessed or uncanonicalized path would produce an override that silently matches nothing".
- Can it happen? Marshalling a Go string can't actually fail, so not in practice.
- Fix: have the function return the error.
buildCodexReviewCmdalready handles failure from the root lookup, so it's a few lines.
It's in the Codex reviewer, which the support-worktree session is editing in the same worktree. Should I fix it here, or leave it to that session?