SuperAPI AI Directory

write-a-hook

General & other

View repository

Updated 10-15 22:11

About this skill

Write a hook

Scaffold a new hook conforming to memo-flow bundle conventions. The skill interrogates you before writing anything; you can't produce an inconsistent hook by accident.

Process

1. Interrogation

Ask the author each question in order. Do not skip any. Do not write files until all answers are collected.

When presenting options via AskUserQuestion, keep the two fields straight: label is the short chip text the user clicks — 1–5 words. description is the 1–2 sentence explanation of what picking the option means. Long prose in label renders as an unreadable chip; it belongs in description.

Hook name (kebab-case, no .sh suffix, e.g. context-monitor):

  • Must be unique among hooks in skills/engineering/memo-hooks/hooks/

Trigger event — pick one:

  • PreToolUse — fires before any tool call; can block (exit 2)
  • PostToolUse — fires after any tool call; advisory only (exit 0)
  • Stop — fires when the agent is about to stop; can block (exit 1)
  • UserPromptSubmit — fires on each user turn; can block (exit 2 surfaces message)
  • PreCompact — fires before context compaction; can block (exit 1)

Tool/pattern matcher (if PreToolUse or PostToolUse):

  • Memo-flow convention: settings.json matcher is always "" (empty / fires on every tool call). Any tool-name or path-pattern filtering happens inside the hook script (typically against tool_input.tool_name / tool_input.file_path).
  • Collect the desired filter here as a script-level constraint, not a settings.json value. The scaffold will encode it inside the script body.
  • Example: filter to Write calls where file_path matches handoff-*.md.

Exit-code contract — pick one:

  • advisory — always exits 0; emits warnings to stderr only
  • blocking — exits 2 (or 1 for Stop/PreCompact) to halt the triggering action; stderr shown to user

Disabled-mode semantics — what does the hook do when "enabled": false in config?

  • Standard answer (used for all existing hooks): exit 0 immediately, no output, no side-effects
  • Confirm or describe a custom disabled behavior

Performance budget — how long may the hook take per fire?

  • fast (< 50 ms): read-only, no network, no subprocess
  • moderate (< 500 ms): small disk I/O, one python3 call
  • slow (> 500 ms): heavy I/O or external subprocess — note this; slow hooks degrade UX

State needs — does the hook read or write state files?

  • None: pure read of stdin + config
  • Read-only: reads an existing state file
  • Read-write: reads and atomically updates a state file (must use flock or temp-and-rename)

Default config — collect the hook-specific config keys (besides enabled), their types, and default values:

  • Example: threshold: number = 130000, mode: string = "auto"

2. Consistency check

Before scaffolding, verify:

  • If exit-code contract is blocking, the script must emit a message before exiting non-zero
  • If state is read-write, the script must use a lock file (flock) or atomic temp-and-rename
  • If a tool/pattern matcher is provided, confirm the trigger is PreToolUse or PostToolUse
  • The hook name does not already exist in skills/engineering/memo-hooks/hooks/

Report any inconsistency and ask the author to clarify before proceeding.

3. Scaffold

Produce all four outputs. Write them in order. Do not omit any.

3a. Hook script

Path: skills/engineering/memo-hooks/hooks/<name>.sh

Follow this template exactly (replace <NAME>, <EVENT>, etc.):

#!/usr/bin/env bash
# <name>.sh — <EVENT> hook: <one-line description>.
#
# <Behavior summary — what it does and when>
#
# Config location: $MEMO_FLOW_CONFIG (env) or ./.claude/memo-flow/config.json (cwd)
# Config key: "<name>"
# Fail-open: missing or unparseable config → treat as enabled with defaults.
# Disabled hook: exits 0 immediately with no output.

set -euo pipefail

# ── find config ───────────────────────────────────────────────────────────────

CONFIG_FILE="${MEMO_FLOW_CONFIG:-./.claude/memo-flow/config.json}"

# ── read config (fail-open) ───────────────────────────────────────────────────

read_config() {
  python3 - "$CONFIG_FILE" <<'PYEOF'
import json, os, sys

config_file = sys.argv[1]
defaults = {
    "enabled": True,
    # ... hook-specific defaults ...
}

if not os.path.exists(config_file):
    print(json.dumps(defaults))
    sys.exit(0)

try:
    data = json.load(open(config_file))
    hook_cfg = data.get("<name>", {})
    if not isinstance(hook_cfg, dict):
        print(json.dumps(defaults))
        sys.exit(0)
    merged = dict(defaults)
    merged.update(hook_cfg)
    print(json.dumps(merged))
except Exception:
    print(json.dumps(defaults))
PYEOF
}

config_json=$(read_config)

enabled=$(python3 -c "import json,sys; print(json.loads(sys.argv[1]).get('enabled', True))" "$config_json")

# disabled → exit 0 immediately (no latency cost)
if [ "$enabled" = "False" ]; then
  exit 0
fi

# ── read event from stdin ─────────────────────────────────────────────────────

event=$(cat)

# ... hook-specific logic ...

3b. config defaults — two files, deliberately different

Memo-flow carries two copies of the per-hook config defaults. They share keys but disagree on enabled. Missing either leaves fresh installs broken.

File 1: memo-hooks/modules/hook-config.sh — _DEFAULTS block

Used by the hook script's fail-open path when config.json is missing or unparseable. Convention: enabled: true (don't break if config disappears).

"<name>": {
  "enabled": true,
  "<key1>": <default1>,
  "<key2>": <default2>
}

File 2: memo-hooks/install.sh — the inline fresh-config heredoc (install.sh:476-488)

Used when install.sh first writes .claude/memo-flow/config.json. Convention: enabled: false (opt-in on fresh install, user enables per hook via /memo-hooks).

"<name>": {
  "enabled": false,
  "<key1>": <default1>,
  "<key2>": <default2>
}

The two enabled values are intentionally inverted. Same keys/values otherwise. If you copy-paste between files, double-check you've flipped enabled.

Known drift: existing hooks already disagree on non-enabled keys (e.g. context-monitor.mode is "auto" in _DEFAULTS but "notify" in install.sh). That's an upstream bug — for new hooks, keep all non-enabled keys identical between the two files.

3c. settings.json template entry + install.sh wiring

The entry that install.sh will register. Show the exact JSON:

{
  "id": "memo-flow:<name>",
  "command": ".claude/memo-flow/hooks/<name>.sh",
  "type": "command"
}

Important: "type": "command", not "stdin". Claude Code silently ignores type: "stdin" entries; install.sh even ships a repair function (_repair_broken_settings_entries) that rewrites stdin → command for memo-flow entries on every run. Always emit command.

Registered under the hook's trigger event key in .claude/settings.json, with an empty matcher (memo-flow convention — see step 1).

The install.sh wiring lives in two locations — they cannot be bundled together because the manifest call references ${settings_rel}, which is only defined between the two blocks.

Location A — after the last "$SETTINGS_SH" insert call (≈ install.sh:518):

<name>_cmd=".claude/memo-flow/hooks/<name>.sh"
<name>_hook="{\"id\":\"memo-flow:<name>\",\"command\":\"${<name>_cmd}\",\"type\":\"command\"}"

"$SETTINGS_SH" insert "$SETTINGS_JSON" "<EVENT>" "" "$<name>_hook"

Location B — after the last existing settings_entry manifest append (≈ install.sh:529), where ${settings_rel} and ${SCOPE} are in scope:

manifest_append_if_absent "$MANIFEST" \
  "{\"id\":\"memo-flow:settings-<name>\",\"kind\":\"settings_entry\",\"target\":\"${settings_rel}\",\"hook_id\":\"memo-flow:<name>\",\"scope\":\"${SCOPE}\",\"customized\":false}"

Note the settings-mutator.sh insert signature: <file> <event> <matcher> <hook-json>. The matcher is the third positional arg and is always "" for memo-flow hooks (script-internal filtering, per step 1).

3d. README row (memo-hooks SKILL.md, Branch A2 opt-in prompt)

Add a row to the Branch A2 sub-question 1 list in skills/engineering/memo-hooks/SKILL.md. That's the bulleted list of hooks shown during fresh install's per-hook opt-in multiSelect. As of writing, the list lives near the line beginning - **\context-monitor`**`.

Use the existing format — kebab-case name in backticks, em-dash, prose description, optional default callout:

- **`<name>`** — <one-line user-facing description of what the hook does and when it fires>. <Optional: default behavior or threshold note>.

Example (existing): - **context-monitor** — Watches your session's token count on every UserPromptSubmit. When you're nearing the context limit, it injects a warning so you can run /handoff before reasoning degrades. Default threshold: 130 000 tokens.

Do not invent a "What gets installed" section — none exists. The opt-in list is the canonical surface where new hooks become discoverable to consumers.

4. Confirm before writing

Show the author a summary of all four outputs. Ask: "Write these files? [y/N]". Only proceed on explicit confirmation.

5. Write files

Write every output. Do not leave any "manual" steps — an unregistered hook is a dead hook.

  1. Write the hook script at skills/engineering/memo-hooks/hooks/<name>.sh and chmod +x it.
  2. Update memo-hooks/modules/hook-config.sh _DEFAULTS (File 1 in step 3b).
  3. Update memo-hooks/install.sh inline fresh-config heredoc (File 2 in step 3b).
  4. Update memo-hooks/install.sh with both wiring blocks from step 3c — Location A (≈ line 518: hook-var assignment + "$SETTINGS_SH" insert) and Location B (≈ line 529: manifest_append_if_absent for the settings entry). Do not collapse them; ${settings_rel} is only defined between the two.
  5. Update memo-hooks/SKILL.md with the README row from step 3d.

6. Done

Confirm all five outputs from step 5 were written. Remind the author:

  • Add an integration test under tests/ for the new hook
  • Run bash bin/run-tests.sh to verify end-to-end
  • Sanity-check by running bash skills/engineering/memo-hooks/install.sh --check-only --project-dir <test-project> against a scratch project to confirm the new hook lands correctly

Opt-in flow: once the hook is registered via install.sh, it automatically participates in the Branch A2 multiSelect prompt the next time a user runs /memo-hooks on a fresh project. Adding a description row to memo-hooks/SKILL.md Branch A2 is required so the entry is populated and meaningful — an undescribed hook appears in the list but gives users no basis for choosing it.

Notes

  • Consumer-local hooks (landing path in the consumer project rather than the bundle) are designed-in but not exposed in v1 — this skill always targets the bundle path.
  • The id field in settings.json entries is tolerated by Claude Code (empirically confirmed) and used by the marker-fence module for idempotent insert/remove.
  • Hook script performance: python3 subprocess startup costs ~30–80 ms. Keep the hot path (disabled check) before any subprocess call.