Unknown hook name fails open instead of blocking the tool (v1.4.0) #6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fleet-integration",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"description": "Integration playbooks for the oleks fleet, distilled from the 2026-07-21 k3s-over-ZeroTier connectivity research (27 incidents, 9 recurring patterns — catalogued on the oleks/cluster wiki page K3s-over-ZeroTier-Connectivity). Core finding: the failures were integration-shaped, not platform-shaped — undeclared state, half-finished migrations, hardcoded environment assumptions, half-implemented lifecycles, watchdog amplification. Six skills cover the levels of the system: integration-preflight (declared-vs-live drift audit before building on top), onboard-host (join a host to the fleet network + k3s), integrate-service (service end-to-end: chart, Flux, ingress, DNS, secrets, DB, TLS, deploy tracking), ephemeral-lifecycle (scale-to-zero credential/member lifecycle rules), add-watchdog (health-check design that doesn't amplify failures), finish-migration (cutover discipline: done means old state REMOVED and declared==live). Playbooks encode failure classes, not current facts — they direct verification against live sources of truth (fleet-modules/cluster-topology.nix, the wiki) rather than restating values that rot.",
|
||||
"author": {
|
||||
"name": "oleks",
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// unknownEntryPoint is the stable stderr marker this binary prints when it is
|
||||
// asked for a hook name it does not have. Tooling greps for it; humans read
|
||||
// the rest of the line.
|
||||
//
|
||||
// It exists because this path can NOT signal through the exit status. Claude
|
||||
// Code reads exit 2 from a PreToolUse hook as "block this tool call", so the
|
||||
// previous os.Exit(2) on an unknown name meant a hooks.json naming a hook the
|
||||
// compiled binary lacks took out the matched tool — for a Bash matcher, every
|
||||
// shell command, in EVERY concurrently running session on the host, not just
|
||||
// the one doing the rebuild.
|
||||
//
|
||||
// That skew is routine rather than exotic: this plugin ships a committed
|
||||
// prebuilt bin/hooks that 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.
|
||||
//
|
||||
// A missing notice is a far smaller loss than a missing tool, so this fails
|
||||
// open. Third independent occurrence of the same defect fleet-wide; see
|
||||
// oleks/claude-plugin-anxious#182 and kotkan/claude-plugin-decision-flow#85.
|
||||
const unknownEntryPoint = "fleet-integration/hooks: unknown-entry-point:"
|
||||
|
||||
// registeredNames returns every entry point this binary actually has, sorted.
|
||||
func registeredNames() []string {
|
||||
names := make([]string, 0, len(dispatch))
|
||||
for k := range dispatch {
|
||||
names = append(names, k)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
// failOpenUnknown reports an unusable invocation on stderr and returns, so the
|
||||
// caller can exit 0. stderr is the right channel: the harness surfaces it as a
|
||||
// diagnostic without blocking, which is exactly the "loud but harmless"
|
||||
// channel this case wants.
|
||||
func failOpenUnknown(name string) {
|
||||
fmt.Fprintf(os.Stderr, "%s %q\n", unknownEntryPoint, name)
|
||||
fmt.Fprintf(os.Stderr, " registered: %s\n", strings.Join(registeredNames(), " "))
|
||||
fmt.Fprintln(os.Stderr, " allowing the call — a hooks.json/binary skew must not block a tool.")
|
||||
}
|
||||
|
||||
// listEntryPoints handles the `list` subcommand, which prints the registered
|
||||
// entry points one per line. It replaces reading a skew off a non-zero exit
|
||||
// status, which is no longer available now that an unknown name fails open.
|
||||
func listEntryPoints() bool {
|
||||
if len(os.Args) == 2 && os.Args[1] == "list" {
|
||||
for _, n := range registeredNames() {
|
||||
fmt.Println(n)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// buildHooks compiles this package into a temp binary. These tests assert on
|
||||
// the PROCESS EXIT STATUS, which is the whole property at issue and which a
|
||||
// function-level test cannot observe.
|
||||
func buildHooks(t *testing.T) string {
|
||||
t.Helper()
|
||||
bin := filepath.Join(t.TempDir(), "hooks")
|
||||
if out, err := exec.Command("go", "build", "-o", bin, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("building test binary: %v\n%s", err, out)
|
||||
}
|
||||
return bin
|
||||
}
|
||||
|
||||
func runHooks(t *testing.T, bin string, args ...string) (stdout, stderr string, code int) {
|
||||
t.Helper()
|
||||
cmd := exec.Command(bin, args...)
|
||||
cmd.Stdin = strings.NewReader("{}")
|
||||
var so, se strings.Builder
|
||||
cmd.Stdout, cmd.Stderr = &so, &se
|
||||
if err := cmd.Run(); err != nil {
|
||||
var ee *exec.ExitError
|
||||
if !errors.As(err, &ee) {
|
||||
t.Fatalf("running %s: %v", bin, err)
|
||||
}
|
||||
code = ee.ExitCode()
|
||||
}
|
||||
return so.String(), se.String(), code
|
||||
}
|
||||
|
||||
// An unknown entry point must NOT exit non-zero. Claude Code reads exit 2 from
|
||||
// a PreToolUse hook as "block this tool call", so a hooks.json naming a hook
|
||||
// this binary lacks would take out the matched tool in every running session
|
||||
// on the host — see the note on unknownEntryPoint.
|
||||
func TestUnknownEntryPointFailsOpen(t *testing.T) {
|
||||
_, stderr, code := runHooks(t, buildHooks(t), "definitely-not-a-registered-hook")
|
||||
if code != 0 {
|
||||
t.Errorf("exit = %d, want 0 (a hooks.json/binary skew must not block a tool)", code)
|
||||
}
|
||||
if !strings.Contains(stderr, unknownEntryPoint) {
|
||||
t.Errorf("stderr missing %q marker, got: %s", unknownEntryPoint, stderr)
|
||||
}
|
||||
if !strings.Contains(stderr, "registered:") {
|
||||
t.Errorf("stderr should name the registered entry points, got: %s", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// A missing entry point is the same fault as an unknown one.
|
||||
func TestMissingEntryPointFailsOpen(t *testing.T) {
|
||||
_, stderr, code := runHooks(t, buildHooks(t))
|
||||
if code != 0 {
|
||||
t.Errorf("exit = %d, want 0", code)
|
||||
}
|
||||
if !strings.Contains(stderr, unknownEntryPoint) {
|
||||
t.Errorf("stderr missing %q marker, got: %s", unknownEntryPoint, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// `list` is how tooling detects a skew now that it cannot read one off a
|
||||
// non-zero exit status.
|
||||
func TestListPrintsEveryRegisteredEntryPoint(t *testing.T) {
|
||||
stdout, _, code := runHooks(t, buildHooks(t), "list")
|
||||
if code != 0 {
|
||||
t.Fatalf("exit = %d, want 0", code)
|
||||
}
|
||||
got := map[string]bool{}
|
||||
for _, line := range strings.Fields(stdout) {
|
||||
got[line] = true
|
||||
}
|
||||
for name := range dispatch {
|
||||
if !got[name] {
|
||||
t.Errorf("list omitted registered entry point %q (got %q)", name, stdout)
|
||||
}
|
||||
}
|
||||
if len(got) != len(dispatch) {
|
||||
t.Errorf("list printed %d names, want %d", len(got), len(dispatch))
|
||||
}
|
||||
}
|
||||
|
||||
// `list` must not shadow a real hook.
|
||||
func TestListIsNotARegisteredHookName(t *testing.T) {
|
||||
if _, clash := dispatch["list"]; clash {
|
||||
t.Fatal(`a hook is registered as "list", which the list subcommand shadows`)
|
||||
}
|
||||
}
|
||||
@@ -32,14 +32,18 @@ var dispatch = map[string]handler{
|
||||
}
|
||||
|
||||
func main() {
|
||||
if listEntryPoints() {
|
||||
return
|
||||
}
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: hooks <hook-name>")
|
||||
os.Exit(2)
|
||||
failOpenUnknown("")
|
||||
return
|
||||
}
|
||||
h, ok := dispatch[os.Args[1]]
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "hooks: unknown hook %q\n", os.Args[1])
|
||||
os.Exit(2)
|
||||
failOpenUnknown(os.Args[1])
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user