Skip to content

fix(guard): resolve cognition paths against the session dir, not the anchor - #50

Merged
ProtocolWarden merged 1 commit into
mainfrom
fix/session-scoped-cognition-paths
Aug 1, 2026
Merged

fix(guard): resolve cognition paths against the session dir, not the anchor#50
ProtocolWarden merged 1 commit into
mainfrom
fix/session-scoped-cognition-paths

Conversation

@ProtocolWarden

Copy link
Copy Markdown
Owner

The defect

ContextGuard joined capsule_path / checkpoint_path / handoff_path onto CL_ANCHOR, while .context/config.yaml documents them as relative to .context/sessions/<CL_SESSION_ID>/. CL_SESSION_ID appeared nowhere in either hook — 11 join sites across pre_tool_use.sh and stop.sh, every one resolving to a directory that does not exist.

Consequences, all silent:

  • 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" rather than enforcing anything

The guard verified CL_ANCHOR was set and then, in practice, did nothing.

Proof, not inspection

A fixture anchor carrying an expired lease:

old hook: exit 0   (silently passed)
new hook: exit 2   {"decision":"block","reason":"...Lease expired at 2020-01-01T00:00:00Z..."}

Backward compatibility

Both hooks resolve against a new COGNITION_ROOT. When CL_SESSION_ID is 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 with json.dumps.

Testing

  • tests/test_hook.py + tests/test_pre_tool_use.py: 26/26 pass
  • Full suite: 17 failures, unchanged from main on this machine (Windows environment: RepoGraph registry, fcntl locking). Not introduced here.

Reviewer note

The CL_SESSION_ID is not set warning fires per tool call, matching the existing warn() call sites in this file, which behave the same way under persistent conditions.

🤖 Generated with Claude Code

…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 ProtocolWarden left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Review pass — two defects found in this change and fixed before merge.

  1. Backward-compat break (would have shipped). Dropping the .context/ prefix from the path defaults is correct for the new base, but a consumer whose config.yaml still 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.

  2. stop.sh would have crashed under set -u. The bulk rewrite introduced COGNITION_ROOT references into stop.sh before 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/... in stop.sh stays anchor-relative — templates are manifest-wide, not per-session. Correct as-is.
  • The SESSION_MARKER hash keys on REPO_ROOT only, so two session ids under one anchor share a marker. Pre-existing, out of scope, not made worse here.
  • The CL_SESSION_ID warning fires per tool call. Matches the existing warn() call sites, which behave identically under persistent conditions.

Hook tests 26/26. Full-suite failure count unchanged from main.

@ProtocolWarden

Copy link
Copy Markdown
Owner Author

CI triage — red, and provably not from this change

Lint (ruff) fails with 205 errors. This diff cannot be the cause:

$ git diff --name-only main...fix/session-scoped-cognition-paths
CHANGELOG.md
adapters/claude/hooks/pre_tool_use.sh
adapters/claude/hooks/stop.sh

Zero Python files. Ruff only checks .py. Confirmed locally too — ruff check src reports an identical 90 errors on main and on this branch, and ruff check . is identical on both.

The flagged file in the log is a test module's import block, untouched here.

Probable root cause, worth its own issue

.github/workflows/ci.yml:33 installs ruff unpinned:

run: pip install "ruff>=0.5"

main last went green on 2026-07-17. Nothing since then has run CI on main itself. An unpinned linter means a new ruff release turns previously-green code red with no commit at all — which matches 205 errors appearing on a repo that was clean in July. Pinning ruff (or --exit-zero on legacy debt with a ratchet) would stop every future PR inheriting an unrelated red.

Test (pytest), License headers, and audit all pass on this branch.

Merging on that basis: the failure is pre-existing and independent of this diff.

@ProtocolWarden
ProtocolWarden merged commit 09931e9 into main Aug 1, 2026
5 of 7 checks passed
@ProtocolWarden
ProtocolWarden deleted the fix/session-scoped-cognition-paths branch August 1, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant