Skip to content

v1.21.2 — the guard asks the repository that owns the write - #24

Merged
sshlg merged 1 commit into
mainfrom
fix/guard-owning-repo
Oct 1, 2026
Merged

sshlg merged 1 commit into
mainfrom
fix/guard-owning-repo

Conversation

@sshlg

@sshlg sshlg commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

The PreToolUse guard decided whether coordination was on from the session's project and then ran the check in a different repository. Three effects, each reproduced against 1.21.1:

  • a git -C <repo> commit (or cd <repo> && git commit) into a repository with no .claude/agent-sync.json was blocked: agent_sync.py guard there exits 2 for "no config", and the guard read that as "no lease";
  • an Edit of a file in another repository ran the guard from the session's cwd, so the path was made relative to the wrong root (../other/docs/ROADMAP.md) and the session's guardedFiles applied instead of the other repository's;
  • a session whose own project has no config was not guarded at all, even when writing a guarded register in a configured repository.

Root cause. plugins/agent-sync/hooks/guard.sh:32 (1.21.1) agent_sync_configured || exit 0, where hooks/_lib.sh:36 checks ${CLAUDE_PROJECT_DIR:-$PWD}/.claude/agent-sync.json; then guard.sh:126 ran (cd "$repo" && python3 "$S" guard "$staged") for a commit, and guard.sh:135 ran python3 "$S" guard "$path" from the session's cwd for an edit.

Fix. agent_sync_owner (hooks/_lib.sh) resolves git -C <dir> rev-parse --show-toplevel (the file's nearest existing directory for Edit/Write, the -C/cd target for a commit) and returns it only when that toplevel carries its own config. No config, or no repository, means allowed; otherwise agent_sync.py guard runs from that toplevel. A missing python3 or git still fails closed in a configured session. A Bash payload without commit now exits before the commit tokeniser starts, because the guard runs in every session now. The lifecycle hooks (SessionStart, renew, SessionEnd) are unchanged: they act on the session's run in the session's project. Renewing leases taken in a second repository is still open as AS-07.

Tests. I wrote check_guard_asks_the_repository_that_owns_the_file first. It runs 15 cases through the real hook, and 7 of them failed on 1.21.1. All 15 pass now. Two self-test plants were added and both are caught: one puts the session lookup back in agent_sync_owner, the other restores the early exit on the session's config.

Gate (local). npm test EXIT=0: validate PASS v1.21.2 (also on Python 3.9.6), SELF-TEST PASS (66 fixtures), claim cell 27, session hooks 13, installer 11. Audit regressions pass, evals OK, pinned house audit 0 GAP / 18 PASS (body 4744/4750 tokens). claude plugin validate --strict passed for . and plugins/agent-sync.

🤖 Generated with Claude Code

The PreToolUse guard decided whether coordination was on from the SESSION's
project (${CLAUDE_PROJECT_DIR:-$PWD}/.claude/agent-sync.json, hooks/_lib.sh
agent_sync_configured, called at the top of guard.sh) and then ran the check
somewhere else:

- a `git -C <repo> commit` / `cd <repo> && git commit` into a repository with
  no config ran `agent_sync.py guard` there, which exits 2 for "no config";
  the guard read that as "no lease" and blocked the commit
- an Edit in another repository ran the guard from the session's cwd, so the
  path was resolved against the wrong root and the wrong guardedFiles applied
- a session whose own project has no config guarded nothing at all

Now agent_sync_owner (hooks/_lib.sh) resolves `git -C <dir> rev-parse
--show-toplevel` — the file's nearest existing directory, or the commit's
-C/cd target — and the check runs from that toplevel only when it carries its
own config; otherwise, and for a path in no repository, the guard allows.
Fail-closed on a missing python3 (and now git) is kept for configured
sessions; a payload without `commit` skips the commit tokeniser.

- check_guard_asks_the_repository_that_owns_the_file: 15 cases through the
  real hook, 7 failed on 1.21.1, all pass
- self-test plants: the session lookup and the early exit put back; both caught
- lifecycle hooks (SessionStart/renew/SessionEnd) unchanged: they act on the
  session's run in the session's project; cross-repo lease renewal stays AS-07
- docs: hooks.md, README, SKILL.md, SECURITY.md, hooks.json description,
  CHANGELOG, verification ledger, backlog

Gate: npm test EXIT=0 — validate PASS v1.21.2 (also on Python 3.9.6),
SELF-TEST PASS (66 fixtures), claim cell 27, session hooks 13, installer 11;
audit regressions pass; evals OK; pinned house audit 0 GAP / 18 PASS;
claude plugin validate --strict passed for . and plugins/agent-sync.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sshlg
sshlg merged commit 6c100d4 into main Oct 1, 2026
2 checks passed
@sshlg
sshlg deleted the fix/guard-owning-repo branch October 1, 2026 05:22
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