RUL-035 — Hooks live at the user level only and scope themselves to the project in cwd
Ruling
Mohamed, 2026-10-06, during the P4 review of plans/ast-daemon-deployment-plan.md,
verbatim: "hooks at user level only but apply to the project in cwd,soo they
apply to all projects, add it to the rules".
Every Claude Code hook is registered at the USER level: ~/.claude/settings.json,
~/.claude/hooks/, or the installed orchestrata plugin's hooks/hooks.json.
Each hook resolves the repository that owns the current working directory at
run time and exits 0 in any other tree (the plugins/orchestrata/project-hooks/_dispatch.sh
pattern), so one registration governs every project a session is opened in.
No repository registers hooks in its own <repo>/.claude/settings.json.
Evidence
- 2026-08-23 (recorded in
_dispatch.sh's header): a session launched from$HOMEnever read<repo>/.claude/settings.json; all 25 guards then registered there were dark while looking installed. Delivery moved to the plugin, but the project file was kept and a stand-down branch (_dispatch.shlines 134-142) chooses which copy runs fromCLAUDE_PROJECT_DIR. - 2026-08-08: an unscoped user-level guard demanded an orchestrata-only pytest plugin of every repo and made another project's suite unrunnable. Scoping to the cwd project is what makes "all projects" safe.
- 2026-10-06, measured in
wt-SPEC-110:.claude/settings.jsonregisters 31 hooks (PreToolUse 15, SessionStart 8, Stop 5, PostToolUse 2, UserPromptSubmit 1);plugins/orchestrata/hooks/hooks.jsonregisters 42;~/.claude/settings.jsonregisters Stop 1 over 46 hook files. The AST-daemon hook is registered at project level AND shipped in the plugin as a byte-identical copy; which copy runs depends on where the session started. A P4 probe was refused by a guard whose prose carried this rule although no ruling recorded it.
Enforcement
- none (judgment)
Mechanism (RUL-014)
Judgment-only at recording time. Candidate gate, a follow-up packet: a
scripts/check_* gate (extend scripts/check_prod_tree_separation.py under
RUL-027, or a new check_hooks_user_level.py) that fails when any tracked
<repo>/.claude/settings.json contains a hooks key. The retirement itself
(delete the 31 project-level registrations after confirming each has a plugin
copy, delete the dispatcher stand-down branch, redeploy through
deploy/install-claude-config.sh) is ADR-0067 P7 scope at the earliest. When
the gate lands, the index row moves from none (judgment) to the gate and
this section names the commit.
Verification
AGENTS.mdHard Rule 9 andCLAUDE.mdKey Rules quote the ruling and name the plugin as the only registration home.docs/adr/0070-hooks-live-at-user-level-only-and-scope-to-the-project-in-cwd.md(Accepted) is the decision record;scripts/check_adr_graph_sync.pyreports ADR-0070OK in sync.- Until the follow-up packet lands,
python3 -c "import json;print(len([h for v in json.load(open('.claude/settings.json'))['hooks'].values() for h in v]))"run in the repo prints the open violation count (31 on 2026-10-06); the packet is done when the file has nohookskey.