Repository navigation
Description
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 pushwithout 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:
- Install
oh-my-opencode(massive opinionated plugin that takes over the entire agent setup just for hook compat) - 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:
- Read hooks from
~/.claude/settings.json(andsettings.local.json) — parse thehooksobject - Map to OpenCode events:
PreToolUse→tool.execute.before— transform tool names (bash→Bash,edit→Edit), pipe JSON stdin, respect exit code 2 as blockPostToolUse→tool.execute.after— same protocol, informationalStop→session.idle— providetranscript_pathwith 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)
- Disable via env var:
OPENCODE_DISABLE_CLAUDE_CODE_HOOKS=1(consistent with existingOPENCODE_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.
Activity
github-actions commentedon Feb 6, 2026
This issue might be a duplicate of or closely related to existing issues. Please check:
While this issue specifically focuses on hook execution (PreToolUse, PostToolUse, Stop) and the protocol for handling them, #10305 discusses more general settings import. These may be complementary features under the broader Claude Code compatibility umbrella.
Feel free to ignore if your specific use case for hook execution isn't addressed by those existing discussions.
nagy135 commentedon Feb 25, 2026
can we use this Issue as a tracker for progress on this? I would definitelly want my hooks to prevent agents from running
git pushor specific npm commands.parkuman commentedon Feb 25, 2026
This would be especially useful in shared repos where some engineers are using Claude Code while others have opted for OpenCode.
marcusquinn commentedon Mar 1, 2026
Use case: Write-time quality enforcement (linting on every edit)
Adding another concrete use case from the aidevops framework.
We evaluated Plankton (write-time code quality enforcement via Claude Code hooks) and found the pattern compelling but not portable to OpenCode without native hook support.
What Plankton does with hooks today:
PostToolUse on Edit/Write — runs 20+ linters (shellcheck, ruff, biome, markdownlint, etc.) on every edited file, collects violations as structured JSON, and feeds them back to the agent immediately. The agent gets write-time feedback and produces better code during generation — not just through post-hoc correction at commit time.
PreToolUse on Edit/Write — blocks edits to linter config files (
.shellcheckrc,.ruff.toml,biome.json, etc.). This prevents agents from "fixing" violations by weakening rules rather than fixing code. A real and common failure mode.PreToolUse on Bash — enforces package manager policies (blocks
pip install, suggestsuv; blocksnpm, suggestsbun).Stop hook — detects if any protected config files were modified during the session via
git diff, prompts user to restore or keep.What we can do without hooks (current workaround):
What we'd do with native hooks:
The same PostToolUse lint-on-edit pattern, but using our existing
linters-local.shinfrastructure. We already have the linters, the structured output, and the fix automation — we just can't trigger them automatically on every tool call.Watching PR #11525 with interest. The proposed
PreToolUse/PostToolUsewith exit code 2 = block + stderr feedback is exactly the protocol we'd use.chore: add write-time quality enforcement rules (t1368)
chore: add write-time quality enforcement rules (t1368) (#2659)
gl33mer commentedon Mar 5, 2026
Use case: Enable rtk-ai/rtk - reduce token use by llm when using a large set of commands
https://github.com/rtk-ai/rtk
Uses Claude hooks to seamlessly use rtk for a large set of commands with large token savings.
https://x.com/chrysb/status/2029278106603929870
yehudacohen commentedon Mar 8, 2026
The
session.idleworkaround for the Stop hook has a specific limitation: timing.When
session.idlefires, the agent loop has already broken and the session is idle. A plugin can callclient.session.chatto re-prompt, but this starts a new prompt invocation from outside the loop, which is why #15267 exists — inopencode runmode the process teardown races against the re-prompt. It also creates a visible user message in the conversation, which is awkward when the intent is a silent continuation check.A hook that fires before the
breaksolves this differently. The re-entry happens inside the existing loop iteration with no new HTTP call and no risk of the process exiting between the event and the response. The injected message uses the same internalcreateUserMessagepath the loop already uses, so it behaves identically to a normal user turn from the model's perspective.I've opened #16626 specifically for this plugin hook and a draft PR at #16598. Keeping it as a draft to get feedback on approach first. This would be a building block for the Stop hook re-activation described here — a plugin implementing Claude Code Stop hook compatibility could use it to inject the hook's stderr output and continue the loop.
marcusquinn commentedon Mar 8, 2026
The
session.stoppingapproach in #16626 / #16598 makes sense — firing before the break avoids the race and the visible re-prompt thatsession.idlerequires. Happy to test the draft PR when ready.For the broader hooks compat: PreToolUse/PostToolUse mapping to
tool.execute.before/tool.execute.afteris the simpler part (tool name translation + stdin protocol + exit codes). Gettingsession.stoppingright first is the right sequencing.yehudacohen commentedon Mar 10, 2026
Thanks @marcusquinn ! Did one small cleanup pass just to fix variable names, retested, and converted to a proper PR.
marcusquinn commentedon Mar 11, 2026
Thanks for pushing this to a PR. The approach is exactly what we need for reliable stop-hook parity without idle-loop races. I can validate it against the aidevops workflow this week (pre/post tool guardrails + stop-time completion checks) and report back with concrete pass/fail cases.
marcusquinn commentedon Mar 11, 2026
Correction: specifically the session.stopping lifecycle event approach. That timing avoids the idle-loop race and keeps stop-hook continuation checks inside the same loop iteration.
PwccaCode commentedon Apr 7, 2026
Question is after all this time we still dont have this ?
Seems like a pretty big deal...
3 remaining items
Necmttn commentedon Jun 21, 2026
A compatibility test matrix would keep this from becoming best-effort glue. For
PreToolUse,PostToolUse, andStop, pin payload shape, stdin encoding, exit-code semantics, stdout/stderr handling, timeout behavior, and whether verdicts block, warn, or mutate input/output.Also include subagent/tool-nesting cases and MCP tools, since Claude hooks often guard by tool name plus transcript path. The migration story is only credible if a hook author can predict each event boundary.
Generated with ax.
lukelabonte commentedon Jul 14, 2026
Use case: Compositional bash approval to reduce permission prompts
When an agent uses compound bash commands like
echo "label" && ls .claude/skills/ | head -20, each chained/piped invocation triggers a separatebash: askpermission prompt. Even when every atomic command in the chain would individually pass the permission rules (all paths within the workspace), the compound command as a whole requires user approval.What a PreToolUse hook could do:
&&,|,;)This would eliminate unnecessary approval prompts for routine verification workflows (checking diffs, listing files, verifying grep output) while still blocking genuinely risky compositions.
Current workaround: Agents avoid compound commands by using single-purpose
bashcalls and the dedicated file-operation tools (grep,glob,read) instead ofrg/ls/catin bash. This works but forces verbose, fragmented agent behavior.shridipavansaber commentedon Aug 9, 2026
Sharing a concrete use case for PostToolUse, in case it helps scope the implementation: tailtest, a plugin that generates and runs tests on every file the agent edits.
What it needs from the hook
After each
Edit,MultiEdit, orWritecall, the handler reads the JSON payload from stdin and pulls the edited path out oftool_input.file_path, falling back totool_input.edits[].file_pathfor the multi-edit shape. It then filters (skips test files, generated code, vendor dirs), queues the file, and runs the suite.Three parts of the Claude Code contract turned out to matter more than the event itself, and they are the parts worth copying deliberately:
hookSpecificOutput.additionalContextputs a message into the model's next turn deterministically, rather than hoping it reads console output.PostToolUsefires after a tool call succeeds andPostToolUseFailureafter one fails. Collapsing them into a single event makes "react to a rejected edit" impossible to express.Why this event and not the neighbours
PreToolUse is too early, the file does not exist yet. A turn-end hook works but batches: ten edited files produce one wall of failures after the fact instead of one correction per edit, inside the loop, while the model still has the context to fix it.
Happy to answer questions about the payload shape or the filtering logic if it is useful.
sjh9714 commentedon Aug 22, 2026
I shipped a userland bridge for the two events that map cleanly today.
https://github.com/sjh9714/opencode-claude-code-hooks
Add the package directly to the OpenCode plugin array.
{ "plugin": ["opencode-claude-code-hooks"] }The bridge reads the existing Claude settings at runtime, maps OpenCode tool names to Claude matcher names, sends the Claude-shaped JSON payload on stdin, honors exit code 2 and structured deny results, applies
updatedInput, and adds PostToolUse feedback to the tool result.Users who want a dry run before installing the bridge can use the compatibility CLI in dsh-movein.
I verified the bridge with real child processes and loaded the npm package through OpenCode 1.18.21. The published package is
opencode-claude-code-hooks 0.1.1.Known limits
session.idlehas the timing problem already documented in this threadifhandlers stay manualfix: address PR review findings on #57
m-naoki-m commentedon Sep 5, 2026
I wrote a small plugin that runs Claude Code hooks from
~/.claude/settings.json, to see how far the event mapping actually gets on 1.18.25. Sharing the measurements because they narrow @yehudacohen's point above to a single line.What works, verified by running
opencode runagainst real hook scripts:tool.execute.beforegives everything the hook needs, and throwing from the handler stops the tool. A hook exiting 2 produces✗ echo DENYTEST failed / Error: DENIED_BY_TEST_HOOK, and the reason reaches the model.tool.execute.afterandchat.message.session.idleand can be given a realtranscript_path: I build the Claude Code JSONL fromclient.session.messagesand my Stop hooks read it fine. One caveat worth writing down — the hook must be spawned synchronously. With an async spawn the transcript gets written but the hook's own output never lands, because run teardown outruns the child process.What does not work is the re-injection, and it is not a plugin-side problem. With debug logging I confirmed the plugin reaches
client.session.promptand no exception is thrown, yet the second turn never runs. The cause is here:https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/cli/cmd/run.ts#L793-L798
The
session.idleplugin handler runs server-side, but the CLI loop breaks on the status event without waiting for it. By the time a plugin starts a new turn the CLI has already left the loop and the process is finishing, so the continuation is never mirrored.promptandpromptAsyncboth lose the race — the former because the await never completes, the latter because nothing waits for it at all.So the gap is not "OpenCode lacks the hook events". Three of the four map well enough today that a plugin covers them. The one missing primitive is a way for a plugin to say "do not settle yet" when idle is reached in non-interactive mode.
I am happy to work on this if you want it, but the fix shape is a design call I would rather not guess at. Two directions I can see:
session.statusbefore leaving the loop, so a plugin-initiated turn is picked up.Either would also serve the Ralph/ULW continuation cases that #15267 touched. Let me know which direction you would accept and I will send a patch.
opencodejr commentedon Oct 29, 2026
Thanks for the detailed write-up! The request is for OpenCode to run the Claude Code hooks in
~/.claude/settings.json(PreToolUse,PostToolUse,Stop) natively, including Stop hooks that exit with code 2 putting the agent back to work. Maintainers will consider it.The event names in the issue (
tool.execute.before,session.idle) are from the v1 plugin API. In OpenCode v2 (@opencode/cli), the plugin API already covers the gap you describe as needing core support:ctx.tool.hook("execute.before", …)can reject a tool call by failing with aTool.Error, which works like exit code 2.ctx.tool.hook("execute.after", …)gets the tool's input and its result or error.ctx.session.hook("experimental.idle", …)runs just before a session goes idle. A message added there (e.g. withctx.session.synthetic) runs in the same execution, so the agent keeps working instead of stopping. The built-in goal plugin uses this.So on v2, a bridge plugin can now handle Stop hooks too, including putting the agent back to work. It would still need to match Claude's matchers, map tool names and inputs (
Bash,Edit, …) and write a Claude-styletranscript_pathfile.balabommablock-cpu commentedon Nov 1, 2026
A product view on what's left, now that v2 covers the hard part.
Where it stands (from the thread and
dev):ctx.tool.hook("execute.before"), fail withTool.Errorctx.tool.hook("execute.after")ctx.session.hook("experimental.idle")+ctx.session.syntheticrunteardown race should go awaySo the plumbing is done. What's left is a few product calls:
Where it lives. v2 already has a first-party Claude compatibility plugin (
config/plugin/compatibility.ts) that finds.clauderoots and loads skills. Hooks fit right next to it, behind the sameclaudeswitch. One home for "use my Claude setup", not a second community bridge.Skills are text. Hooks run programs. This is the big one. A cloned repo can ship
.claude/settings.jsonwith a hook that runs on the first tool call. Claude Code asks you to trust a folder before that happens. Suggest:~/.claude/settings.jsonhooks: on, with a one-line notice the first time ("Running 3 Claude Code hooks from ~/.claude")..claude/settings.jsonhooks: off until you trust that folder once.Copy Claude's rules exactly, even the odd ones. Exit 2 blocks, exit 1 doesn't (tailtest's note above shows how easy that is to get wrong). Same stdin JSON, same tool names (
Bash,Edit), a realtranscript_path. The whole value is "my hooks just work", so any "improvement" here is a bug for someone. Necmttn's test matrix is the right way to hold that line.Hooks never widen permissions. A hook's "allow" shouldn't skip an OpenCode
ask. It can only make things stricter. (The userland bridge already does this; worth making it the rule.)Say who stopped the agent. When a hook blocks a tool or sends the agent back to work, show it:
Blocked by hook check-push.sh: run tests first. Otherwise it looks like the model being stubborn, and people go debugging the wrong thing.Smallest v1: PreToolUse, PostToolUse, Stop and UserPromptSubmit, command hooks only, global on, project behind trust. Everything else stays in the "No equivalent found" list until someone needs it.
Happy to help write the conformance cases for 3 if that's useful.
fferdgelis commentedon Nov 7, 2026
Hi team. As users and developers working with OpenCode every day, we keep hitting the same stopper: Claude Code, Codex and Antigravity all support command hooks (PreToolUse, PostToolUse, Stop), and OpenCode does not. That forces us to keep a separate JS plugin just for OpenCode.
PR #52765 already implemented this, but it was closed by the cleanup bot without a maintainer review. Could someone please take a look and reopen it, or tell us what design you would accept?
OpenCode is open source, so we would be glad to send the code ourselves and adapt it to your guidelines.
Hamza-Abili commentedon Nov 10, 2026
For anyone landing here before native support lands: I maintain a community plugin that maps the Claude Code hook surface onto OpenCode's V1/V2 plugin APIs — @hamzaabili/opencode-claude-hooks (
0.1.0, MIT).It covers
SessionStart,UserPromptSubmit,PreToolUse,PostToolUse/PostToolUseFailure,Stop,SubagentStart/SubagentStop,PreCompact/PostCompact,SessionEndandNotification, reads settings/plugin/skill/subagent hooks, and adds trust fingerprints, an environment allowlist, timeouts and a compatibility doctor.Honest gaps:
mcp_toolandagenthandlers,Setup/PermissionRequest, V2< 2.2.1, Windows. It is a bridge for people who need existing hooks today, not a replacement for native support.https://github.com/Hamza-Abili/opencode-claude-hooks
@ArtyMcLabin if the event mapping or trust model is useful for the native design, I am happy to share notes.
j-256 commentedon Nov 11, 2026
Hamza just barely beat me to it. 😂 @Hamza-Abili I found some handling issues I'd like to report, but your documented path via GitHub private security reporting is disabled.
I have to figure out whether to try to compete or cannibalize my project and commit to yours. 😊 I took the route of supporting Codex as well as Claude Code – not sure if there's a reason to keep it exclusive to Claude.
I'm more than happy to help get hooks into OpenCode but not sure the right way to contribute. I got deeper than I should've before checking what work people had already done on this, so I may just build out my solution and folks can make of it what they will...
Hamza-Abili commentedon Nov 11, 2026
Hey @j-256 — thanks, and sorry about the broken reporting path. That's on me:
SECURITY.mdlinks to private advisories but the feature wasn't enabled on the repo. It's on now, so the findings can go through https://github.com/Hamza-Abili/opencode-claude-hooks/security/advisories/new (I've also replied on the repo issue).On competing vs contributing: I'd rather not see you discard work, and I'm not attached to owning the only bridge. If you want to compare scope, I'm happy to look at what you've built and work out whether to merge efforts, split the surface, or at minimum share notes so we don't duplicate the same translation layer.
The pieces I'd most like help with are the V2 paths (Stop/SubagentStop via
experimental.idle), themcp_tool/agenthandlers, and a conformance matrix against Claude's protocol — but I'm open to whatever shape you think is best, including a PR to the existing plugin.Either way, thanks for the detailed writeup in this thread; it's useful for the native design too. The userland bridge and native support in opencode#12472 aren't mutually exclusive.
Hamza-Abili commentedon Nov 11, 2026
Hey @j-256 — quick update now that both reports are fixed and published.
onFailure: "block"bypassed by JSON output — fixed in 0.1.1.ok:falseresponse — fixed in 0.1.2.I also did a compatibility/hardening pass on the rest of the surface and shipped 0.1.3:
statusMessageis honoured for command, HTTP and prompt hooks;SubagentStopinstead ofStop;PostToolUseFailureincludes theerrorfield in the hook payload;denytoask;doctor --trust none|promptis parsed correctly and no longer consumes the next flag.If you have a minute, testing 0.1.3 against your harness would be genuinely useful — especially the prompt/Stop paths and the V1 child-session mapping. If anything still fails, open an issue or an advisory and I'll fix it.
On the compete-vs-contribute question: no pressure either way. If you want to bring your work over, I'm happy to review a PR or compare scope first; if you'd rather keep your own implementation, that's fine too. The two reports were already valuable, and the credit is on both advisories.
Releases: v0.1.1 · v0.1.2 · v0.1.3
neriousy commentedon Nov 12, 2026
Thanks for the suggestion. This isn't something we plan to build, so closing to keep the tracker focused.