fix(guard): resolve cognition paths against the session dir, not the anchor - #50
Conversation
…anchor
ContextGuard joined capsule/checkpoint/handoff paths onto CL_ANCHOR, while
config.yaml documents them as relative to .context/sessions/<CL_SESSION_ID>/.
CL_SESSION_ID appeared nowhere in either hook, so all three directories
resolved to paths that do not exist and every guard silently found nothing:
- lease expiry never fired (the handoff was never located)
- worker forbidden_paths never loaded
- require_capsule: true was a landmine, not a feature — it would have
blocked every tool call with "No active capsule found"
Both hooks now resolve against COGNITION_ROOT, and the path defaults drop
their `.context/` prefix to match the config convention. When CL_SESSION_ID
is unset, pre_tool_use falls back to the anchor's .context/ and warns rather
than blocking, so existing consumers degrade instead of hard-failing.
Verified by fixture rather than inspection: an anchor carrying an expired
lease passes the previous hook (exit 0) and blocks on this one (exit 2).
Also fix block(): it hand-built its JSON, so a reason containing a Windows
path emitted an unescaped backslash and the payload failed to parse — the
operator saw a blocked call with no reason at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ProtocolWarden
left a comment
There was a problem hiding this comment.
Review pass — two defects found in this change and fixed before merge.
-
Backward-compat break (would have shipped). Dropping the
.context/prefix from the path defaults is correct for the new base, but a consumer whoseconfig.yamlstill explicitly pins the old.context/active/value would have resolved to<anchor>/.context/.context/active/— silently re-creating the exact bug this PR fixes, for the users least likely to have migrated. Fixed by stripping a leading.context/from configured values, and verified against a legacy-layout fixture: identical block behaviour on old and new hooks. -
stop.shwould have crashed underset -u. The bulk rewrite introducedCOGNITION_ROOTreferences intostop.shbefore adding its definition — an unbound variable on every session end. Caught by syntax + functional check, definition added.
Checked and deliberately left alone:
Template: .context/templates/...instop.shstays anchor-relative — templates are manifest-wide, not per-session. Correct as-is.- The
SESSION_MARKERhash keys onREPO_ROOTonly, so two session ids under one anchor share a marker. Pre-existing, out of scope, not made worse here. - The
CL_SESSION_IDwarning fires per tool call. Matches the existingwarn()call sites, which behave identically under persistent conditions.
Hook tests 26/26. Full-suite failure count unchanged from main.
CI triage — red, and provably not from this change
Zero Python files. Ruff only checks The flagged file in the log is a test module's import block, untouched here. Probable root cause, worth its own issue
run: pip install "ruff>=0.5"
Merging on that basis: the failure is pre-existing and independent of this diff. |
The defect
ContextGuard joined
capsule_path/checkpoint_path/handoff_pathontoCL_ANCHOR, while.context/config.yamldocuments them as relative to.context/sessions/<CL_SESSION_ID>/.CL_SESSION_IDappeared nowhere in either hook — 11 join sites acrosspre_tool_use.shandstop.sh, every one resolving to a directory that does not exist.Consequences, all silent:
forbidden_pathsnever loadedrequire_capsule: truewas a landmine, not a feature — it would have blocked every tool call with "No active capsule found" rather than enforcing anythingThe guard verified
CL_ANCHORwas set and then, in practice, did nothing.Proof, not inspection
A fixture anchor carrying an expired lease:
Backward compatibility
Both hooks resolve against a new
COGNITION_ROOT. WhenCL_SESSION_IDis unset it falls back to the anchor's.context/and warns, rather than blocking.Path defaults drop their
.context/prefix to match the config convention. A consumer whose config still pins the old.context/active/values would otherwise have resolved to<anchor>/.context/.context/active/, so configured values now have a leading.context/stripped. Verified: a legacy layout + legacy config with no session id blocks identically on both the old and new hook.Also fixed
block()hand-built its JSON, so a reason containing a Windows path emitted an unescaped backslash (\U) and the payload failed to parse — a blocked call reached the operator with no reason attached. Now serialized withjson.dumps.Testing
tests/test_hook.py+tests/test_pre_tool_use.py: 26/26 passmainon this machine (Windows environment: RepoGraph registry,fcntllocking). Not introduced here.Reviewer note
The
CL_SESSION_ID is not setwarning fires per tool call, matching the existingwarn()call sites in this file, which behave the same way under persistent conditions.🤖 Generated with Claude Code