Hooks
What stm runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /plugin marketplace add matterhornso/subscribetome > /plugin install stm@subscribetome
Ships with stm. Installing the plugin gets these hooks.
What fires, and when
PreToolUse
- Matches
Bash|Write|Edit|MultiEdit|NotebookEdit"${CLAUDE_PLUGIN_ROOT}"/hooks/pretooluse.sh
PostToolUse
- Matches
Bash"${CLAUDE_PLUGIN_ROOT}"/hooks/posttooluse.sh
UserPromptSubmit
Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.
"${CLAUDE_PLUGIN_ROOT}"/hooks/userpromptsubmit.sh
SessionStart
Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.
"${CLAUDE_PLUGIN_ROOT}"/hooks/sessionstart.sh
Where it lives
- hooks/posttooluse.shRunsGitHub
Read the script
#!/usr/bin/env bash # subscribetome PostToolUse hook — flags command output that leaked a key. # Thin wrapper; all logic is in src/hooks.ts (postToolUse). ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" . "$ROOT/hooks/resolve-bun.sh" if ! stm_resolve_bun; then stm_bun_missing "PostToolUse hook" exit 0 fi exec "$BUN" "$ROOT/src/cli.ts" hook posttooluse - hooks/pretooluse.shRunsGitHub
Read the script
#!/usr/bin/env bash # subscribetome PreToolUse hook — placeholder injection + leak guards. # Thin wrapper; all logic is in src/hooks.ts (preToolUse). ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" . "$ROOT/hooks/resolve-bun.sh" if ! stm_resolve_bun; then stm_bun_missing "PreToolUse hook" exit 0 fi exec "$BUN" "$ROOT/src/cli.ts" hook pretooluse - hooks/resolve-bun.shGitHub
Read the script
#!/usr/bin/env bash # Shared bun resolution for every stm shell entrypoint. # # WHY THIS EXISTS: hooks are spawned by the agent host (Claude Code / Codex), # whose environment is NOT the user's interactive shell. A GUI/Dock launch or # an IDE host commonly has a minimal PATH with no Homebrew on it. The old # one-liner # # BUN="$(command -v bun || echo "$HOME/.bun/bin/bun")" # # had two defects: it only knew the official-installer location (missing # Homebrew's /opt/homebrew/bin/bun — which README.md itself recommends), and # it never checked that the fallback was executable, so a miss produced a # cryptic `exec: ... cannot execute` instead of a usable message. # # The failure was not cosmetic. With bun unresolvable the UserPromptSubmit # paste guard exited 0 without scanning — a security control silently failing # OPEN, with the user still believing pasted keys were being blocked. # # Mirrors resolveBunPath() in src/agents/codex-mcp-install.ts, which already # solved this for the Codex MCP surface. # Sets $BUN to an executable bun. Returns 0 on success, 1 if none found. stm_resolve_bun() { local candidate # 1. PATH first — covers the normal case and any custom install. candidate="$(command -v bun 2>/dev/null || true)" if [ -n "$candidate" ] && [ -x "$candidate" ]; then BUN="$candidate" return 0 fi # 2. Known install locations, most likely first. Each is checked for # executability, so a stale/partial install falls through instead of # becoming an exec failure. for candidate in \ "$HOME/.bun/bin/bun" \ "/opt/homebrew/bin/bun" \ "/usr/local/bin/bun" \ "/home/linuxbrew/.linuxbrew/bin/bun" \ "/snap/bin/bun" do if [ -x "$candidate" ]; then BUN="$candidate" return 0 fi done return 1 } # Loud, actionable message when bun can't be found. $1 = hook/entrypoint name. # Never silent: a guard that cannot run must say so rather than let the user # assume they are protected. stm_bun_missing() { local who="${1:-stm}" cat >&2 <<'EOF' [stm] Cannot find the `bun` runtime — stm is NOT running. EOF echo "[stm] (entrypoint: ${who})" >&2 cat >&2 <<'EOF' [stm] Looked on PATH and in: ~/.bun/bin, /opt/homebrew/bin, /usr/local/bin, [stm] /home/linuxbrew/.linuxbrew/bin, /snap/bin [stm] Install bun (https://bun.sh) or, if it is already installed somewhere [stm] else, symlink it into one of those locations. [stm] Until then: placeholders are NOT substituted and the paste guard does [stm] NOT scan your prompts. Run `stm doctor` for the full picture. EOF } - hooks/sessionstart.shRunsGitHub
Read the script
#!/usr/bin/env bash # subscribetome SessionStart hook — injects stm usage guidance into every # session so the model knows how to use stm-managed keys with no user setup. # Thin wrapper; all logic is in src/hooks.ts (sessionStart). ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" . "$ROOT/hooks/resolve-bun.sh" if ! stm_resolve_bun; then stm_bun_missing "SessionStart hook" exit 0 fi exec "$BUN" "$ROOT/src/cli.ts" hook sessionstart - hooks/userpromptsubmit.shRunsGitHub
Read the script
#!/usr/bin/env bash # subscribetome UserPromptSubmit hook — blocks a raw key pasted into the chat. # Thin wrapper; all logic is in src/hooks.ts (userPromptSubmit). ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" . "$ROOT/hooks/resolve-bun.sh" if ! stm_resolve_bun; then stm_bun_missing "UserPromptSubmit hook" exit 0 fi exec "$BUN" "$ROOT/src/cli.ts" hook userpromptsubmit
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Your API keys in the OS keychain. Your AI coding agent uses them — without ever seeing them. The model writes {{stm:openai:default}}. The real key is swapped in at the moment the command runs.
Repo: matterhornso/subscribetome

