Skip to content

Native Claude Code hooks compatibility (PreToolUse, PostToolUse, Stop) #12472

@ArtyMcLabin

Description

@ArtyMcLabin

Summary

OpenCode already has excellent Claude Code compatibility for rules (CLAUDE.md), skills (~/.claude/skills/), and can be disabled via OPENCODE_DISABLE_CLAUDE_CODE* env vars. However, Claude Code's hooks system (PreToolUse, PostToolUse, Stop defined in ~/.claude/settings.json) is not supported natively.

Problem

Users who run both Claude Code and OpenCode maintain hooks in ~/.claude/settings.json that enforce guardrails:

  • PreToolUse — Block dangerous operations (e.g., git push without tests, direct prod DB access, editing governed files without reading the architecture skill first)
  • PostToolUse — Track state (e.g., mark that tests were run, mark that code files were edited)
  • Stop — Verify the dev-loop SOP was followed before the agent stops (tests run, committed, deployed, QA'd)

These hooks are Python/bash scripts that receive JSON on stdin (tool_name, tool_input, transcript_path) and use exit codes to signal behavior (0=allow, 2=block+feedback via stderr).

Without native support, users must either:

  1. Install oh-my-opencode (massive opinionated plugin that takes over the entire agent setup just for hook compat)
  2. Write a custom bridge plugin (works for PreToolUse/PostToolUse but Stop hooks lose transcript access and can't re-activate the agent)

Proposed Solution

Add native Claude Code hook execution, similar to how CLAUDE.md and skills are already handled:

  1. Read hooks from ~/.claude/settings.json (and settings.local.json) — parse the hooks object
  2. Map to OpenCode events:
    • PreToolUsetool.execute.before — transform tool names (bashBash, editEdit), pipe JSON stdin, respect exit code 2 as block
    • PostToolUsetool.execute.after — same protocol, informational
    • Stopsession.idle — provide transcript_path with full session transcript (tool calls + assistant text), and critically: if exit code 2, re-inject stderr as a prompt to continue (this is how Claude Code's Stop hooks enforce SOP completion)
  3. Disable via env var: OPENCODE_DISABLE_CLAUDE_CODE_HOOKS=1 (consistent with existing OPENCODE_DISABLE_CLAUDE_CODE_* pattern)

Key Gap: Stop Hook Re-activation

The most critical missing piece is Stop hook re-activation. In Claude Code, when a Stop hook exits with code 2, the stderr message is injected back as a prompt and the agent continues working. This is how dev-loop SOPs are enforced — the hook says "you edited code but didn't run tests" and the agent resumes to run tests.

OpenCode's session.idle event is fire-and-forget. The feature request specifically asks for a mechanism where a session.idle handler can signal "don't stop, inject this message instead" — either via return value or a callback.

Protocol Reference

Claude Code hook stdin format:

{
  "session_id": "...",
  "cwd": "/path/to/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "git push origin main" },
  "transcript_path": "/tmp/transcript.jsonl"
}

Exit codes:

  • 0 = allow (stderr is informational, shown to LLM but doesn't block)
  • 2 = block (PreToolUse) or re-activate (Stop) — stderr is the feedback message

Why Native vs Plugin

A plugin can handle PreToolUse/PostToolUse adequately, but Stop hook re-activation requires OpenCode core support — plugins can't force the agent to resume from session.idle. This is why native support matters.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions