Home / Projects / Orchestrata Ultimate / Decisions / ADR index / ADR-0070
ADR-0070: Hooks live at the user level only and scope themselves to the project in cwd, so they apply to every project
| field | value |
|---|---|
| Status | Accepted — Accepted (ruling, Mohamed, 2026-10-06: "hooks at user level only but apply to the project in cwd,soo they apply to all projects, add it to the rules") |
| Date | 2026-10-06 |
| Register | factory (docs/adr, 4-digit ids) |
| Source | docs/adr/0070-hooks-live-at-user-level-only-and-scope-to-the-project-in-cwd.md — permalink at 9e3535d35 (unchanged on master since that commit) |
| Wiki register entry | ADR-0070 (wiki status: Accepted) |
The record
Status: Accepted (ruling, Mohamed, 2026-10-06: "hooks at user level only but apply to the project in cwd,soo they apply to all projects, add it to the rules") · Date: 2026-10-06
Context
Claude Code loads hook registrations from two places: the user level
(~/.claude/settings.json, ~/.claude/hooks/, and every installed plugin's
hooks/hooks.json) and the project level (<repo>/.claude/settings.json).
The project file is loaded only when the session's own project directory IS
that repo. Measured 2026-08-23 and recorded in
plugins/orchestrata/project-hooks/_dispatch.sh: a session launched from
$HOME never read <repo>/.claude/settings.json, so all 25 guards then
registered there (AST mandate, no-xdist, no-doubles, banned-deps, the gate
battery, design-before-build, every Stop guard) were dark while looking
installed. Delivery moved to the orchestrata plugin, which loads in every
session; _dispatch.sh scopes each guard to the repo that owns the current
working directory and exits in any other tree.
Measured 2026-10-06 in wt-SPEC-110:
| home | registrations |
|---|---|
<repo>/.claude/settings.json (project level) |
31 (UserPromptSubmit 1, SessionStart 8, PreToolUse 15, Stop 5, PostToolUse 2) |
plugins/orchestrata/hooks/hooks.json (user level, via plugin, commit 83c604886 installed) |
42 (SessionStart 11, UserPromptSubmit 11, PreToolUse 13, PostToolUse 3, Stop 2, SessionEnd 1, PermissionRequest 1) |
~/.claude/hooks/ + ~/.claude/settings.json (user level) |
46 hook files; settings registers Stop 1 |
Two delivery paths therefore coexist for the same guards. _dispatch.sh
lines 134-142 STAND DOWN when CLAUDE_PROJECT_DIR equals the repo and the
project file exists, to avoid double firing. The result is the defect the P4
reviewer hit on 2026-10-06: the AST-daemon hook is registered at project
level (.claude/settings.json:26) AND shipped in the plugin as a byte-identical
copy, and which copy runs depends on where the session was started. A probe
that would have exercised the plugin path was refused by a guard carrying the
text "all hooks and tools must be at the user level, not project level, but
they apply to the project in cwd" — a rule that existed as guard prose and
nowhere in the rulebook.
Decision (Mohamed, ruling)
Hooks live at the USER level only: ~/.claude/settings.json,
~/.claude/hooks/, and the installed orchestrata plugin's hooks/hooks.json.
Each hook scopes itself to the project that owns the current working
directory (the _dispatch.sh pattern: resolve the repo from cwd, exit 0 in
any other tree), so the same registration applies to every project a session
is opened in. No project registers hooks in its own <repo>/.claude/settings.json.
Alternatives rejected
- Keep both homes with the stand-down branch. Two copies of every guard, a branch that decides which one runs from an environment variable, and a guard whose behaviour depends on the launch directory. This is the state measured above; it is what produced the reviewer's finding.
- Project level only. Measured dark for every
$HOME-rooted session (2026-08-23); the rules then protect only sessions that happen to start in the repo. - User level, unscoped. Measured failure 2026-08-08: an orchestrata-only pytest plugin was demanded of every repo and made another project's suite unrunnable. Scoping to the cwd project is what makes "all projects" safe.
Consequences
<repo>/.claude/settings.jsonloses itshookskey; the 31 project-level registrations are retired in favour of the plugin copies already shipped (any of the 31 not yet inhooks.jsonis added there first). The stand-down branch in_dispatch.sh(lines 134-142) becomes dead and is removed. This is a follow-up packet (ADR-0067 P7 scope at the earliest, or its own packet), shipped throughdeploy/install-claude-config.sh/install-tools.shso the plugin copy is the deployed one.- Gate: a
scripts/check_*gate (candidate: extendscripts/check_prod_tree_separation.pyRUL-027, or a newcheck_hooks_user_level.py) fails when any tracked<repo>/.claude/settings.jsoncontains ahookskey. Until it lands, wiki ruling RUL-035 isenforcement: none (judgment). - Guard headers that carry the rule as prose (the probe refusal text) cite this ADR instead.
- P4 (
plans/ast-daemon-deployment-plan.md) is unchanged by this ruling: the AST-daemon hook's plugin copy is already byte-identical to the project copy; retiring the project copy is the follow-up above, not a P4 blocker. - Applies to every project on this host, not only orchestrata-ultimate: the
memory note
hooks-user-level-onlycarries it across sessions.
Provenance
- Mohamed, 2026-10-06, verbatim: "hooks at user level only but apply to the project in cwd,soo they apply to all projects, add it to the rules".
- Trigger: P4 review round 2 (plan
plans/ast-daemon-deployment-plan.md, commit 13e551b46) routed the project-level registration of.claude/hooks/start-ast-daemon.shto Mohamed. - Evidence:
plugins/orchestrata/project-hooks/_dispatch.shheader (2026-08-23 sentinel measurement, 2026-08-08 unscoped-guard failure); registration counts measured 2026-10-06 above.
Related ADRs
- Also mentioned: ADR-0067
Rulings cited
- RUL-027: RUL-027 — Production runs its own checkout, never the development tree
- RUL-035: RUL-035 — Hooks live at the user level only and scope themselves to the project in cwd