Skip to content

feat(hooks): move hooks config to dedicated hooks.json, cut .claude/ dependency - #51

Merged
LeXwDeX merged 1 commit into
devfrom
feat/hooks-config-independence
Jul 3, 2026
Merged

feat(hooks): move hooks config to dedicated hooks.json, cut .claude/ dependency#51
LeXwDeX merged 1 commit into
devfrom
feat/hooks-config-independence

Conversation

@LeXwDeX

Copy link
Copy Markdown
Owner

⚠️ Breaking Changes

Two things reviewers and users MUST know:

  1. .claude/ directories and settings.json hooks field are no longer read. Users with existing Claude Code hook configs need to run /import-claude-hooks to migrate. A one-time deprecation warning is logged if hooks are found in old locations.
  2. Global ~/.config/opencode/hooks.json changes require restart. Only project/worktree hooks.json files are hot-reloaded. Global hooks load once at startup.

Summary

Establishes OpenCode's own hooks identity by moving hooks configuration from settings.json (mixed with other settings, 6-layer chain including .claude/) to dedicated hooks.json files in OpenCode-owned directories only.

Config Location: 6 layers → 2 dedicated files

FileScopeHot-reload
~/.config/opencode/hooks.jsonGlobal❌ Startup only
.opencode/hooks.jsonProject✅ Polling (2s)
<worktree>/.opencode/hooks.jsonWorktree✅ Polling (2s)

Format: top-level event keys (no wrapper)

{
"PreToolUse": [{ "matcher": "Bash", "hooks": [{"type": "command", ...}] }],
"SessionStart": [{ "matcher": "*", "hooks": [{"type": "command", ...}] }]
}

Legacy {"hooks": {...}} wrapper tolerated via graceful degradation.

Migration: /import-claude-hooks

Built-in slash command (agent-guided, zero new TypeScript):

  1. Scans ~/.claude/settings.json, .claude/settings.json, .claude/settings.local.json
  2. Also scans old-format .opencode/settings.json hooks field
  3. QA-guided: presents each hook for user review (import / skip / edit)
  4. Handles ${CLAUDE_PLUGIN_ROOT} path migration (.claude/ → .opencode/)
  5. Writes to corresponding hooks.json files
  6. Updates AGENTS.md managed section

Agent Self-Awareness

AGENTS.md gains <!-- Hooks_START --> / <!-- Hooks_END --> managed section. The import command writes a summary table of active hooks. Since AGENTS.md is loaded as instructions at session start, the agent knows what hooks exist in its environment without reading hooks.json directly — full config details read on-demand.

Implementation Details

loadChain() refactor (settings.ts)

  • Removed 3 .claude/ + 3 .local path entries
  • Changed remaining entries from settings.jsonhooks.json
  • Worktree support preserved (<worktree>/.opencode/hooks.json)
  • Top-level event parsing with wrapper fallback
  • VALID_HOOK_EVENTS whitelist + Array.isArray filter — defends against `` and non-event keys being treated as matchers (would cause TypeError on .length)
  • Deprecation scan with one-time warning per file

Hot-reload (hot-reload.ts)

  • Project-level only (global is startup-only)
  • Interval polling (2s mtime check) instead of fs.watch — WSL2 DrvFs /mnt/* mounts have unreliable inotify
  • Detects file deletion (mtime from >0 to 0 triggers reload)
  • Debounce (500ms) + min-interval (1s) + reschedule (not drop)
  • HotReloadHandle.close() interface unchanged

Built-in command (command/index.ts)

  • src/command/template/import-claude-hooks.txt — prompt template
  • Registered in Default.IMPORT_HOOKS alongside INIT, REVIEW, GOAL

Tests: 21 hook tests (was 0)

FileTestsCoverage
load-chain.test.ts12Paths, merge order, format, wrapper, deprecation, .claude/ exclusion, $schema filtering, polling reload
settings-dedup.test.ts4Per-session dedup, SessionEnd eviction
settings-hot-reload.test.ts2Polling reload, close() stops reload
Deletion test1File deletion triggers reload (P2)

Total: 21 pass, 0 fail, typecheck clean.

OpenSpec

Full spec-driven artifacts committed alongside code:

  • proposal.md — FABLE5 code-verified
  • design.md — 7 decisions (D1-D7)
  • specs/hooks-config/spec.md — 5 requirements / 18 scenarios
  • tasks.md — 43/43 tasks complete

Ready to archive via /opsx:archive hooks-config-independence after merge.

…dependency
BREAKING: .claude/ directories and settings.json hooks field are no
longer read. Run /import-claude-hooks to migrate existing Claude
Code hook configurations.
Config location (6 layers → 2 dedicated files):
~/.config/opencode/hooks.json (global, loaded once at startup)
.opencode/hooks.json (project, hot-reloaded via polling)
<worktree>/.opencode/hooks.json (worktree, hot-reloaded)
Key changes:
- loadChain reads hooks.json from OpenCode-owned dirs only (.claude/
and .local variants removed)
- Top-level event format (no wrapper); legacy {"hooks":{...}} tolerated
- readJSON filters via VALID_HOOK_EVENTS whitelist + Array.isArray
- Deprecation warning for hooks left in old settings.json
- Hot-reload: project-level only, interval polling (2s) instead of
fs.watch (WSL2 DrvFs reliability); detects file deletion
- Global hooks.json is startup-only (no hot-reload), restart required
- Built-in /import-claude-hooks command for QA-guided migration
- AGENTS.md managed section (<!-- Hooks_START/END -->) for agent
self-awareness of configured hooks
OpenSpec: hooks-config-independence (spec-driven, 43/43 tasks complete)
Tests: 21 hook tests (load-chain + settings-dedup + hot-reload)
@LeXwDeX
LeXwDeX merged commit 929aa9f into devJul 3, 2026
4 checks passed
@LeXwDeX
LeXwDeX deleted the feat/hooks-config-independence branch July 3, 2026 07:50
Sign up for freeto 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

@LeXwDeX