# hooklib A small **library of composable Claude Code hooks**. Each hook is a drop-in script; a shared router dispatches one event to all of them. Adding a hook is just dropping an executable script into the matching directory — no wiring. ## Layout ``` hooks/ hooks.json # registers the router per event (currently PreToolUse:Bash) dispatch.sh # router: feeds the event JSON to every hooks/.d/*.sh pretooluse.d/ 10-nix-deploy-output.sh ``` ### How dispatch works `dispatch.sh ` reads the event JSON from stdin once and replays it to each script in `hooks/.d/` (lexical order). A sub-hook that wants to act prints a hook JSON response to stdout and exits 0; the **first** sub-hook to emit non-empty output wins and the router forwards it verbatim. If nobody speaks up, the router is silent (Claude Code treats that as "allow"). Scripts are numbered (`10-`, `20-`, …) to make ordering explicit. ## Hooks ### `nix-deploy-output` (PreToolUse / Bash) **Problem:** `nix run .#deploy` (deploy-rs) shows **empty output** in Claude Code. deploy-rs and nix render progress as an ANSI spinner on a TTY and log to stderr; captured non-interactively, that collapses to a blank. **Fix:** the command must fold stderr into stdout and defeat TTY detection so the tools emit plain, line-buffered logs. The canonical form is: ``` nix run .#deploy 2>&1 | cat ``` The hook inspects any `nix run .#deploy…` command. If it is already in the output-safe form (`2>&1` **and** a pipe into `cat`/`tee`/`less`) it passes through untouched. Otherwise it **denies** the call and returns the corrected command (preserving any `--` flags / host target) for Claude to run instead. ## Adding another hook Drop `hooks/.d/NN-name.sh` (executable) that reads the event JSON on stdin and prints a hook response only when it wants to intervene. If the event isn't `PreToolUse`, add it to `hooks/hooks.json` pointing at `dispatch.sh `. ## Install (local dev) ``` claude plugin install hooklib@oleks-local ```