Unknown hook name fails open instead of blocking the tool (v1.4.0) #6

Merged
oleks merged 1 commits from fix/5-unknown-hook-fail-open into main 2026-08-19 00:02:41 +03:00
Owner

main() answered an unknown or missing os.Args[1] with os.Exit(2), which Claude Code reads from a PreToolUse hook as "block this tool call" — so a hooks.json/binary skew removed the matched tool in every running session on the host.

The file already failed open one line later ("an unreadable stdin is silence, exit 0"), so this was the one branch violating the contract the rest of it kept.

Unknown/missing entry point now prints a loud unknown-entry-point: diagnostic to stderr naming the key and every registered name, then exits 0. A new list subcommand keeps the skew detectable.

Verified before/after on the same probe: bin/hooks not-a-real-hook exited 2, now exits 0. go test passes.

Fixes #5

`main()` answered an unknown or missing `os.Args[1]` with `os.Exit(2)`, which Claude Code reads from a PreToolUse hook as "block this tool call" — so a `hooks.json`/binary skew removed the matched tool in every running session on the host. The file already failed open one line later ("an unreadable stdin is silence, exit 0"), so this was the one branch violating the contract the rest of it kept. Unknown/missing entry point now prints a loud `unknown-entry-point:` diagnostic to stderr naming the key and every registered name, then exits 0. A new `list` subcommand keeps the skew detectable. Verified before/after on the same probe: `bin/hooks not-a-real-hook` exited 2, now exits 0. `go test` passes. Fixes #5
oleks added 1 commit 2026-08-19 00:01:46 +03:00
main.go answered an unknown or missing hook name with a non-zero exit. Claude
Code reads exit 2 from a PreToolUse hook as "block this tool call", so a
hooks.json naming a hook the compiled binary does not have took out the matched
tool in EVERY running session on the host, not just the one doing the rebuild.
This plugin wires one PreToolUse hook.

That skew is routine rather than exotic: bin/hooks is a committed prebuilt
binary CI does not rebuild, and oleks-local is a directory source, so a live
session sees an edited hooks.json the instant it is written, against whatever
binary is on disk.

- failopen.go: unknown/missing entry point prints a loud `unknown-entry-point:`
  diagnostic to stderr naming the key and every registered name, then exits 0.
  stderr is the right channel: surfaced to the user, blocks nothing.
- failopen.go: `list` subcommand prints the registered entry points, so tooling
  can still detect a skew now that it cannot read one off a non-zero exit.
- failopen_test.go: end-to-end tests on the built binary — the property under
  test is the process exit status, which a function-level test cannot see.

Third independent occurrence of this defect fleet-wide, after
oleks/claude-plugin-anxious#182 and kotkan/claude-plugin-decision-flow#85; the
shared root is the hooks-to-go template every port copied
(oleks/claude-plugin-hooks-to-go#30).

Verified before/after on the same probe: `bin/hooks not-a-real-hook` exited 2,
now exits 0 with the marker on stderr. go test passes.
oleks merged commit 195eadff5d into main 2026-08-19 00:02:41 +03:00
oleks deleted branch fix/5-unknown-hook-fail-open 2026-08-19 00:02:42 +03:00
Sign in to join this conversation.