Feature request : project-scoped hooks (.claude/hooks.json)
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
Global hooks in ~/.claude/settings.json fire on every project, every session, every prompt — even when the current directory has
nothing to do with the plugin that registered them.
Building AZCLAUDE (a Claude Code marketplace plugin with 691 tests), every hook script must start with guard logic:
```javascript
const goalsPath = path.join('.claude', 'memory', 'goals.md');
if (!fs.existsSync(goalsPath)) process.exit(0); // not my project, bail
This creates three real problems we hit in production:
- Unnecessary process spawns — Node.js starts and immediately exits on every prompt in every non-matching project
- Side-effect risk — before we added guards, our hooks were creating ops/observations/ directories in arbitrary user directories.
A bug in a guard means a hook runs in the wrong place.
- Plugin isolation — multiple plugins share the same global settings.json hooks object. Our installer had to implement per-key
merging to avoid destroying other plugins' hooks. This was flagged as a CRITICAL security finding in our own code review.
Commands (.claude/commands/) and agents (.claude/agents/) are already project-scoped. Hooks are the missing piece.
Proposed Solution
Support a .claude/hooks.json at the project root, same schema as
the hooks section of settings.json:
{
"UserPromptSubmit": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "node .claude/hooks/user-prompt.js" }] }
],
"PostToolUse": [
{ "matcher": "Write|Edit", "hooks": [{ "type": "command", "command": "node .claude/hooks/post-tool-use.js" }] }
]
}
Merge behavior:
- Global hooks from
~/.claude/settings.json— fire everywhere (existing) - Project hooks from
.claude/hooks.json— fire only in that project (new) - Both merge at runtime — no replacement, no conflict
This follows the existing pattern: .claude/commands/ and .claude/agents/
are already project-scoped. Hooks are the only feature that requires global
configuration to work per-project.
Alternative Solutions
Current workaround: Every hook checks for project markers (goals.md, CLAUDE.md) and exits early if not found. Works, but:
- Every plugin author must implement this independently
- Forgotten guards cause cross-project side effects (we shipped this bug)
- Node.js still spawns on every prompt just to exit immediately
Convention-based guards: Fragile. No enforcement. Each plugin invents its own guard pattern.
Plugin-scoped hooks in marketplace format: The marketplace hooks/hooks.json exists but still registers globally via
${CLAUDE_PLUGIN_ROOT}. It doesn't scope execution to matching projects.
Priority
High - Significant impact on productivity
Feature Category
Other
Use Case Example
- I install AZCLAUDE via the Claude Code marketplace — it registers 3 global hooks (UserPromptSubmit, PostToolUse, Stop)
- I open a different project that doesn't use AZCLAUDE
- On every prompt, UserPromptSubmit spawns Node.js, checks for goals.md, doesn't find it, exits
- On every Write/Edit, PostToolUse spawns Node.js, checks for goals.md, doesn't find it, exits
- These are wasted process spawns that slow down every non-AZCLAUDE session
With project-scoped hooks:
- AZCLAUDE installs .claude/hooks.json in the project directory during /setup
- Hooks only fire when Claude Code is running inside that project
- Other projects are completely unaffected — zero process spawns, zero guards needed
- Multiple plugins can coexist without modifying the same global config file
Additional Context
Real-world evidence: https://github.com/haytamAroui/AZ-CLAUDE (marketplace plugin, v1.0.0, 691 tests) had to implement:
- Per-key hook merging in the installer to avoid destroying other plugins' hooks
- Project existence guards in all 3 hook scripts
- Path validation to prevent side effects in non-AZCLAUDE directories
All of this complexity disappears with project-scoped hooks.
The marketplace plugin format already supports project-scoped commands and agents. Hooks are the only piece that remains
global-only.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗