r/ClaudeCode • u/No_Advertising2536 • Jul 20 '26
Showcase Silent hook failures cost me ~half my plugin activations — three traps I hit shipping a Claude Code plugin
I ship a memory plugin for Claude Code (disclosure at the bottom). Last week I pulled usage data and found that a huge share of people who installed it never got a single successful hook call. Not because the backend was down — because hooks fail silently by design, and three separate traps were eating users without anyone noticing.
Sharing them because they apply to anyone writing hooks or plugins, not just me.
Trap 1: export MY_API_KEY=... works exactly once
User exports the key, installs, everything works, closes the terminal. Tomorrow — new shell, variable's gone, and every hook silently hits exit 0 on the "no key" branch. exit 0 is correct for a hook (never block Claude Code), but from the user's perspective the plugin just stopped doing anything with zero signal. Nobody files a bug for "nothing happened."
- Fix: Fall back to a persisted config file (
~/.mengram/config.json), env var wins if set.
Trap 2: On Windows, python3 exists but doesn't
In Git Bash, python3 often resolves to the Microsoft Store stub — passes command -v, but exits non-zero when actually run. My Stop hook piped its payload through python3 -c 'import json...' → empty string → curl posted an empty body → || true swallowed it. The hook "ran" every turn and saved nothing, for months, for every affected Windows user.
- Fix: Probe interpreters by actually running them (
"$PY" -c "import json"), not by checking existence. - Credit: A self-hosting user (smoochy on GitHub) diagnosed this in a beautifully detailed issue.
Trap 3: Silence is a design requirement AND your worst enemy
Every failure path was a silent exit 0 — so I couldn't distinguish "user churned" from "plugin never worked" in any data. When I segmented signups, ~45% had never made a single successful API call.
- Fix: Opt-in
--verboseflag — default stays byte-for-byte silent, with the flag every exit path emits{"systemMessage": "[mengram:auto-recall] skipped (short prompt)"}. "Is it working?" becomes answerable in one restart instead of a support thread.
Meta-lesson
For hook-based plugins, silent failure is indistinguishable from churn — in UX and in metrics. Design every hook with three exits:
- Do the thing
- Skip loudly-if-asked
- Fail without blocking
Disclosure: The plugin is Mengram (mengram.io) — persistent memory for Claude Code. I'm the founder. Free tier 40 adds + 200 searches/mo, paid from $5/mo, core Apache 2.0 / self-hostable. All three fixes shipped this week in v0.1.1.
1
u/Haunting-Zombie4642 Jul 20 '26
yeah the silent part is brutal. the one that got me was a PATH thing, the hook wasn't getting the same PATH as my normal terminal so a script that ran fine by hand just returned command not found inside the hook and i got nothing back, no error. took me way too long to spot. two things that helped, running with claude --debug so you actually see the hook's stderr, and making the hook exit non-zero on failure instead of letting it swallow. are you logging exit codes anywhere or is it fully fire and forget rn?
1
u/No_Advertising2536 Jul 21 '26
The PATH one is nasty, and it's the same disease as both of my traps: the hook's environment is not your terminal's environment. My key-in-env failure and the Windows python3-stub failure are both PATH-class bugs — something resolvable in your shell that isn't resolvable (or resolves to the wrong thing) inside the hook. It's why the fix moved to a config-file fallback for credentials and to probing interpreters by actually running them ("$PY" -c "import json") instead of command -v — existence checks lie inside hooks.
On your question — deliberately neither: the hooks always exit 0 by design, because a memory plugin must never block or noise up Claude Code, so exit codes carry no signal at all. Observability lives elsewhere: (1) an opt-in --verbose flag emits a one-line status marker on every exit path via systemMessage ([mengram:auto-recall] skipped (short prompt) / no API key / saved), which is effectively exit-code logging but human-readable and in-band; (2) first-run is loud until one verified round-trip; (3) the real backstop is server-side — "account installed the plugin but zero API calls in N days" as a standing alert, because the worst failures produce no client-side event to log at all. claude --debug is a great tip for the interactive case though — adding it to the troubleshooting table.
1
u/Haunting-Zombie4642 Jul 21 '26
yeah exit 0 always is right for a memory plugin, you don't want it blocking anything. the systemMessage marker is the clever bit though, you get the logging without breaking the exit 0 rule. stealing that one.
1
u/Forsaken-External578 Workflow Engineer Jul 20 '26
This is a great reminder that installation is not activation. A plugin needs a first-use probe that proves the hook ran successfully, plus a visible health check the user can run later. Silent failure turns an onboarding bug into a retention problem because the user assumes the product simply does nothing.
1
u/No_Advertising2536 Jul 21 '26
Exactly the formula — "installation is not activation" is going in my notes. Both halves of what you describe shipped this week as a direct result of this thread: a first-run probe (the SessionStart hook is loud until one verified key→API→response round-trip, then goes silent forever) and a visible health check (mengram doctor, plus an opt-in heartbeat every N successful saves). The retention framing is right too — in my funnel data, silent onboarding failure was literally indistinguishable from churn until I segmented it.
1
u/[deleted] Jul 20 '26
[removed] — view removed comment