Hooks Create logo

Hooks Create

Community
coleam00
hooks-create

Author a working Claude Code hook from a plain-English description of what it should guarantee or do. You describe the behavior ("never let the agent edit my migrations", "don't stop until the tests pass", "log every command"); this skill picks the right lifecycle event, writes the hook script, and wires it into .claude/settings.json. Use when you want a deterministic guarantee or automation in your AI Layer and don't want to write the hook by hand. The meta-tool for the hooks primitive.

Overview

Publishercoleam00
Repositoryskills
Skill namehooks-create
Stars
678
Forks
184
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by coleam00 on GitHub. Read the source before you install it.

Installation

Install the Hooks Create AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/coleam00/skills/tree/main/.claude/skills/hooks-create
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/coleam00/skills.git /tmp/skills
mkdir -p .claude/skills
cp -r /tmp/skills/.claude/skills/hooks-create .claude/skills/hooks-create
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Hooks Create in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Hooks Create on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Hooks Create is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Create Hook: Turn an Idea Into a Working Claude Code Hook

What the user wants the hook to do: $ARGUMENTS

If that is filled in, treat it as the behavior spec and start from it. Don't re-ask what they already told you; only ask to pin down the gaps (the exact paths / commands / patterns, and whether it must block). If it is blank, start by asking what the hook should guarantee or do (Workflow step 1).

What a hook is (30-second intro)

A hook is deterministic code that fires automatically on a Claude Code lifecycle event - before a tool runs, after it runs, when the agent tries to finish, when a session starts, and so on. Unlike a skill or a subagent, the agent does not choose to invoke a hook - it fires whether the model "remembers" or not. That is the whole point: a rule asks the agent to behave; a hook guarantees it, at the tooling layer the model can't talk its way around.

The user brings the idea ("the agent should never read my .env"); this skill writes the code and wires it in. They don't need to know Python.

It's all composition - a hook is just a small script the harness runs at a defined moment, configured in .claude/settings.json. You're adding one more deterministic guarantee to the AI Layer.

The one thing to get right: which event, and can it block?

The behavior the user wants maps to one lifecycle event. Pick by when it should fire and whether it must stop something:

The user wants to…EventCan it block?
Stop the agent from doing something (read a secret, edit a protected path, run a destructive command)PreToolUse ⭐Yes - block the tool before it runs
React after an action (auto-format an edited file, log a command, inject context)PostToolUseNo - the tool already ran; observe / format / inject only
Guarantee work isn't "done" until a check passes (don't stop until tests/lint/types are green)Stop (or SubagentStop)Yes - block the stop and send the agent back to work
Gate or scan the user's prompt before the model sees itUserPromptSubmitYes - block the prompt; can also inject context
Load context every time a session startsSessionStartNo - inject context only
Get notified when the agent needs you / finishesNotification (or Stop)No - side-effect only (desktop/Slack/sound)
Snapshot state before context compactionPreCompactNo

Pre = guarantee/gate. Post = react/log. If the user's goal is "make sure X never happens" or "don't finish until Y," it's a blocking hook (PreToolUse / Stop / UserPromptSubmit). If it's "do Z when W happens," it's an observe/react hook (PostToolUse / SessionStart / Notification).

Required reading (do this first)

The hook event list and the exact stdin/stdout contract evolve - don't rely on a snapshot. Before writing, fetch the current docs and confirm the event name, its input fields, and its control protocol:

Use WebFetch on these and verify against what you're about to write. If the fetch fails, proceed from the canonical events in the table above and say so in your report so the user can double-check.

The execution protocol (how a hook talks to Claude Code)

  • Input: Claude Code passes a JSON object on stdin - always includes session_id, cwd, hook_event_name; event-specific fields like tool_name + tool_input (tool events), prompt (UserPromptSubmit), source (SessionStart).
  • Output / control:
    • exit 0 - allow / success. For UserPromptSubmit and SessionStart, anything printed to stdout is injected into the agent's context.
    • exit 2 - block. The action is prevented and whatever you print to stderr is fed back to the agent as the reason, so it adapts. (Only blocking-capable events honor this - see the table.)
    • any other exit code - non-blocking error; shown to the user, execution continues.
    • Advanced (optional): instead of exit codes, print a JSON object on stdout - e.g. {"decision":"block", "reason":"…"} (Stop), or {"hookSpecificOutput":{"hookEventName":"PreToolUse", "permissionDecision":"deny","permissionDecisionReason":"…"}}. Prefer the simple exit-code form unless the user needs to modify input/output or inject context with additionalContext. Confirm field names against the fetched docs.

Workflow

1. Understand the idea (start from $ARGUMENTS; ask only to fill gaps)

Start from what the user already described in $ARGUMENTS (the user may not be technical). Pin down two things in plain language, asking only for what is missing:

  • What should happen or be prevented, and when (before/after an action, at finish, at session start)?
  • How precisely should it match? ("any .env file", "the migrations/ folder", "rm -rf", "my test command exits non-zero"). Get the concrete file paths / commands / patterns - the guarantee is only as good as what it matches, so don't guess. If the ask is vague, propose a concrete interpretation and confirm.

2. Read the docs

Fetch the hooks reference/guide (above) and confirm the target event's name, stdin fields, and control protocol.

3. Pick the event (and matcher)

Use the table to choose the single event. Choose a matcher that scopes it tightly - for tool events, the tool name(s) (e.g. "Bash", "Edit|Write", "mcp__.*"); empty/"*" means every occurrence. Don't fire on everything if the goal is specific.

4. Write the hook script

  • Default to a uv single-file Python script at .claude/hooks/<event_snake_case>.py - a uv run --script shebang plus inline # /// script metadata, so it needs no install and no project venv of its own. Use another language only if the user asks.
  • Read the JSON from stdin, do the check, and:
    • to block: print a clear reason to stderr and sys.exit(2);
    • to allow: sys.exit(0) (optionally print context to stdout for the injecting events).
  • Fail open. Wrap the body so any unexpected error exits 0 (a broken hook must never brick the user's session). The only intentional non-zero exit is the deliberate exit 2 block.
  • Keep it lean and readable - the user will want to tweak the matched paths/commands later.
  • If a hook already exists for that event, extend it rather than overwrite (add your check; keep theirs).
  • Stop / SubagentStop only: check stop_hook_active first and sys.exit(0) when it is true. Without that guard the hook blocks the stop, the agent works, tries to stop again, is blocked again - a loop.

⚠️ Running a project command from a hook (read this before writing one)

The hook itself runs under uv run, in an isolated ephemeral environment. Its interpreter is NOT the project's interpreter and does NOT have the project's dependencies. So:

  • NEVER rebuild the command with sys.executable or a bare ["python", "-m", "pytest", ...] list. That runs the hook's throwaway python, which has no pytest, no project packages, nothing. The command fails with No module named … every single time - so a "don't finish until tests pass" hook blocks on green as readily as on red, and reports a nonsense reason. It looks like it works. It does not.
  • DO run the user's command verbatim, as a shell string, in the project directory - with uv's ephemeral venv stripped AND the project's own venv put first. Both steps are required. shell=True alone is not enough: uv run puts its throwaway interpreter first on PATH and sets VIRTUAL_ENV. But removing uv's venv does not activate the project's - a hook is not the user's shell, so .venv was never on PATH to begin with, and python falls through to whatever global interpreter the machine has. That global one has a different (often broken) set of packages, so the hook exits 2 on a perfectly green suite and blames some unrelated module. Copy this helper as-is:
    python
    TEST_COMMAND = "python -m pytest -q"   # exactly what the user typed; the one line they'll edit
    
    def _project_env(project_root: Path) -> dict:
        """os.environ with uv's ephemeral venv removed and the PROJECT's venv first."""
        env = os.environ.copy()
        ephemeral = env.pop("VIRTUAL_ENV", None)
        parts = env.get("PATH", "").split(os.pathsep)
        if ephemeral:                                    # 1. uv's throwaway venv, out
            drop = {os.path.join(ephemeral, "Scripts"), os.path.join(ephemeral, "bin")}
            parts = [p for p in parts if p not in drop]
        for candidate in (".venv", "venv"):              # 2. the project's own venv, first
            for bindir in ("Scripts", "bin"):
                venv_bin = project_root / candidate / bindir
                if venv_bin.is_dir():
                    env["VIRTUAL_ENV"] = str(project_root / candidate)
                    parts.insert(0, str(venv_bin))
                    env["PATH"] = os.pathsep.join(parts)
                    return env
        env["PATH"] = os.pathsep.join(parts)
        return env
    
    root = Path(hook_input["cwd"])          # the project root Claude Code passes in
    result = subprocess.run(
        TEST_COMMAND, shell=True, capture_output=True, text=True,
        cwd=str(root), env=_project_env(root),
    )
    Put the command in a single named constant at the top of the file so the user can edit one obvious line.
  • If the command must run from a subdirectory (a monorepo, or a project whose test config lives deeper - e.g. app/backend/), ask for that, and pass it: cwd=Path(hook_input["cwd"]) / "app/backend". Getting this wrong produces a hook that always blocks, which the user will read as "hooks are broken."

5. Wire it into settings.json

Edit .claude/settings.json (create it if absent). Merge into any existing hooks block - never clobber other events or other hooks on the same event. Shape:

json
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Edit|Write|Read|Bash",
        "hooks": [ { "type": "command", "command": "uv run .claude/hooks/pre_tool_use.py" } ] }
    ]
  }
}

6. Prove it yourself, then explain and warn

Run the hook before you hand it over. Feed it a sample event on stdin and check the exit code - do not ship a hook you have only read. A hook that always blocks, or never blocks, looks identical to a working one until it fires at the wrong moment.

bash
# should ALLOW (exit 0)
echo '{"session_id":"t","cwd":"<project-root>","hook_event_name":"Stop","stop_hook_active":false}' | uv run .claude/hooks/stop.py; echo "exit=$?"
  • For a command-running hook (tests/lint/types), this is mandatory and you must check both directions: it exits 0 while the command passes, and exits 2 once it genuinely fails. If it exits 2 in both states, the command is not resolving - re-read the warning in step 4 about sys.executable.
  • For a blocking guard, feed it one payload that should be blocked and one that should pass.
  • If a check comes back wrong, fix the script and re-run before reporting success.

Then:

  • Tell the user what you built, in plain words: which event, what it guarantees, and the one line they'd change to adjust it.
  • Give them a way to prove it in the agent: for a blocking hook, an action that should be blocked ("ask me to read the env file - watch it refuse"); for an observe hook, where the output lands (the log, the notification).
  • Report what you verified, and say plainly if you could not verify something.
  • Security note (always say this): a hook runs arbitrary code automatically, with your credentials, on every matching event. Review hooks like you review CI config; only run hooks you trust. (Same caution as MCP servers.)

Quality checks

  • ✅ The behavior maps to the right event, and a blocking goal uses a blocking-capable event (PreToolUse / Stop / UserPromptSubmit) - not PostToolUse.
  • ✅ The matcher is scoped to what the user actually meant (not firing on everything by accident).
  • ✅ The script fails open - any error exits 0; the only exit 2 is the intended block, with a clear stderr reason.
  • ✅ Any project command runs verbatim via shell=True in the project cwd, with uv's ephemeral venv stripped and the project's own venv put first on PATH - never rebuilt with sys.executable (the hook's own interpreter has none of the project's dependencies, and the global one has the wrong ones).
  • ✅ A Stop / SubagentStop hook honors stop_hook_active so it cannot loop.
  • ✅ settings.json was merged, not overwritten; existing hooks still present.
  • ✅ You ran the hook and confirmed it exits 0 when it should allow and 2 when it should block - not just read it.
  • ✅ The user got a plain-English explanation + a test + the security note.

Notes

  • Hooks are the deterministic floor of the AI Layer - use them for the non-negotiables (secrets, protected paths, "don't finish until green"), not for things a rule or skill handles well enough.
  • A blocking hook's coverage is only as good as its matcher - it guarantees the hook runs, but you decide what it catches. Be honest with the user about the edges (e.g. a .env matcher won't catch a base64'd read).
  • Keep hooks fast - they run on the matched event every time. Heavy work belongs in an async hook or a skill.

Frequently asked questions

What does the Hooks Create AI skill do?

Author a working Claude Code hook from a plain-English description of what it should guarantee or do. You describe the behavior ("never let the agent edit my migrations", "don't stop until the tests pass", "log every command"); this skill picks the right lifecycle event, writes the hook script, and wires it into .claude/settings.json. Use when you want a deterministic guarantee or automation in your AI Layer and don't want to write the hook by hand. The meta-tool for the hooks primitive.

Why use Hooks Create on TypingMind?

Because you install it once and use it with any model. Hooks Create is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Hooks Create in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/coleam00/skills/tree/main/.claude/skills/hooks-create. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Hooks Create?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Hooks Create?

As many as you like. As long as a model supports skills, you can use Hooks Create with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Hooks Create AI skill free?

Yes. It is published on GitHub by coleam00 under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇