Skip to content
Security
Hook

Hooks

What stm runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
stm
45 commands4 hooks
Install
> /plugin marketplace add matterhornso/subscribetome
> /plugin install stm@subscribetome

Ships with stm. Installing the plugin gets these hooks.

What fires, and when

PreToolUse

  • MatchesBash|Write|Edit|MultiEdit|NotebookEdit"${CLAUDE_PLUGIN_ROOT}"/hooks/pretooluse.sh

PostToolUse

  • MatchesBash"${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
Read hooks/hooks.json

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.

Ships withstm

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.

Get the whole plugin
Stats
4
Stars
1
Forks
Active
Maintenance
TypeScript
Language
MIT
License
18d ago
Last commit
4mo ago
Created

Repo: matterhornso/subscribetome