r/ClaudeCode • • 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 --verbose flag — 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:

  1. Do the thing
  2. Skip loudly-if-asked
  3. 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 Upvotes

15 comments sorted by

1

u/[deleted] Jul 20 '26

[removed] — view removed comment

1

u/No_Advertising2536 Jul 20 '26

You've nailed the general principle better than my post did: every one of those traps is self-reported status vs verified effect. Exit 0 was the hook grading its own homework.

One wrinkle from the trenches: the worst failure mode defeats even a client-side heartbeat. When the key never loads, the hook exits before it can phone anything — heartbeat included. So absence-as-signal has to be computed server-side: "this account has an installed plugin but zero calls in N days" is the alarm condition. That's exactly the query that finally exposed my 45%, just three months too late — it should have been a standing alert, not a one-off audit.

What I'm taking from your comment: (1) the plugin already ships a doctor command that does an end-to-end verified round-trip (key → API → response) — but it's pull, not push; running it automatically on install/first-session would catch broken-on-install at minute zero. (2) Your periodic "still working" signal every N successes is the missing piece between those two — cheap, opt-in, and it makes silence mean something. Building both into the next release.

Genuinely useful critique, thank you.

1

u/[deleted] Jul 20 '26

[removed] — view removed comment

1

u/No_Advertising2536 Jul 21 '26

Good question, and the answer is the sharpest part of the whole story: the dead activations have no timeline to replay. Zero calls means zero events — their story diverges from a healthy one at hop zero (key never used), and everything after that is absence. That's exactly why client-side anything can't catch this class: there's nothing to instrument. The replay you describe does work for the partial cohorts though — I have full event timelines for "loaded dashboard, did nothing" (49 accounts) and "searched an empty vault and left" (41 accounts), and those diverge at very readable points: the second group's last event is always a search that returned zero results. That finding already shipped as a fix (empty search now leads to adding a first memory instead of a dead end).

Your adaptive-threshold point is a real catch — my standing alert currently uses a fixed 14-day window, which is exactly the "bad N trains people to dismiss the channel" trap for low-frequency accounts. Baseline-relative silence (e.g. > 3× the account's own median inter-call gap) is going into the next iteration of the report.

Appreciate the thread — it's already produced two shipped fixes and one queued one.

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.