← Back to Guides
GuideFor: AI Engineers, ML Engineers, Platform Engineers, AI Systems Architects

Superpowers Plugin for Claude Code: Install and Verify

Install it, prove which skills actually fire, and measure what it costs you - then switch it off where it does not belong.

#tutorial#intermediate#claude-code#superpowers#plugins#agent-skills

What you'll build with the Superpowers plugin

By the end you will have Superpowers installed and provably active, and a detector that names which skills actually fired in any headless Claude Code run. You will build one small feature twice: once by hand, once by handing it to the plugin's own test-first cycle, so you can see the difference rather than take my word for it. You will also have a token budget for what the plugin charges you every session, whether or not it does anything.

This assumes you use Claude Code regularly and know that skills, hooks, and subagents exist, but have never installed a plugin or opened a SKILL.md. Allow about 45 minutes.

Here is the finished detector, run against a transcript of a real Claude Code session:

code
hook     SessionStart:startup   injected 3472 bytesplugin   superpowers 6.2.0      superpowers@claude-plugins-officialskill    superpowers:brainstormingverdict  SKILL FIRED

And here is the same detector, same plugin, same machine, on a different request:

code
hook     SessionStart:startup   injected 3472 bytesplugin   superpowers 6.2.0      superpowers@claude-plugins-officialskill    none firedverdict  LOADED BUT NO SKILL FIRED

Superpowers is a set of instructions asking a model to behave a certain way, and a model follows those instructions most of the time. Your plugin list cannot tell you which time you got.

Verified against Claude Code 2.1.228, superpowers 6.2.0 (superpowers@claude-plugins-official, user scope), git 2.51.0.windows.1, Python 3.13.9, and pytest 8.4.2 on 2026-08-12. Seventeen of the 58 code blocks were not machine-verified, because they make billable Claude API calls or read files that only such a call produces: the two preview outputs directly above, all four blocks of step 8, the Claude run in each of steps 6, 7 and 11 together with the command that reads what it produced and that command's output, and the post-disable check at the end of step 12 with its output. I ran those by hand, and every transcript and figure shown for them is a real capture from those runs. Everything else was executed or structurally checked on a clean rebuild.

Prerequisites

Level: intermediate. You should be comfortable running Claude Code from a terminal and reading JSON.

RequirementPinned versionWhy
Claude Code2.1.228The claude plugin subcommands and the shell hook key both need a recent build
Superpowers6.2.0Released late July 2026
Git2.51.0Marketplaces are git repositories. On Windows, Git Bash is also what runs the plugin's hook
Python3.13.9For the detector and the worked example
pytest8.4.2For the test-first cycle

Superpowers is MIT licensed and free. Running the steps that call Claude costs whatever your normal Claude Code usage costs. The Claude runs behind this tutorial cost me 2.63 USD in total, and two of them were me getting step 12 wrong.

Every shell block in this tutorial is POSIX. On Windows, run all of them in Git Bash, which ships with Git for Windows. PowerShell and cmd do not have wc, rm -rf, or < /dev/null, and several steps use them.

Windows readers have a second reason to care: Git for Windows is not optional here. Superpowers 6.2.0 runs its startup hook through Git Bash, and the fallback when it cannot find bash is to exit quietly. You get no error and no Superpowers. This is covered in detail in When it breaks.

On cost. Steps 6, 7, 8 and 11, and the last check in step 12, call the Claude API and cost money. If you would rather not spend it, skip them: nothing later in the tutorial reads run-fired.jsonl, run-skipped.jsonl, run-tdd.jsonl, run-disabled.jsonl, greet.py, or the design document, and the closing verification does not touch any of them. You will finish with a working detector, a tested feature, and a scoped-off plugin. What you will not have is live proof that a skill fired on your machine, which is the one thing steps 6 and 11 exist to give you.

Install pytest and confirm the whole toolchain in one command:

code
pip install "pytest==8.4.2"claude --version && git --version && python --version && python -m pytest --version

Expected output, with your own paths and patch versions:

code
2.1.228 (Claude Code)git version 2.51.0.windows.1Python 3.13.9pytest 8.4.2

If claude --version prints nothing or errors, stop here and fix that first. Every later step shells out to it.

The mental model: how Superpowers makes a model change its behaviour

Superpowers is simpler and stranger than "a set of skills", and the difference decides what you can and cannot do with it.

A Claude Code plugin can ship skills, agents, hooks, MCP servers, and LSP servers. Superpowers 6.2.0 ships exactly two of those - 14 skills and one hook - and the hook is what makes the skills matter.

Normally a skill sits dormant. Claude Code shows the model a listing of every available skill name and a short description, and the model decides on its own whether to invoke one through the Skill tool. That listing is the mechanism behind skills as a production knowledge layer. That is a suggestion, and models routinely skip suggestions.

Superpowers changes the odds by adding a SessionStart hook, which is policy-as-code applied to an agent in its simplest form. Every time a session starts, clears, or compacts, that hook runs a shell script which reads one specific skill file, using-superpowers/SKILL.md, and prints it as JSON on standard output. Claude Code takes that text and injects it directly into the model's context, wrapped in an EXTREMELY_IMPORTANT tag. The injected text tells the model, in unusually forceful language, that it must check for an applicable skill before doing anything at all.

A hook injects a paragraph. The paragraph does the rest.

mermaid
flowchart LR
    A["Session starts"] --> B["SessionStart hook<br/>runs run-hook.cmd"]
    B --> C["Reads using-superpowers<br/>SKILL.md from disk"]
    C --> D["Prints JSON to stdout"]
    D --> E["Claude Code injects it<br/>into model context"]
    E --> F["Model reads:<br/>check skills first"]
    F --> G["Model calls the<br/>Skill tool"]
    G --> H["Full skill body loads<br/>on demand"]

    style A fill:#95A5A6,color:#FFFFFF
    style B fill:#4A90E2,color:#FFFFFF
    style C fill:#4A90E2,color:#FFFFFF
    style D fill:#4A90E2,color:#FFFFFF
    style E fill:#7B68EE,color:#FFFFFF
    style F fill:#7B68EE,color:#FFFFFF
    style G fill:#6BCF7F,color:#2C2C2A
    style H fill:#6BCF7F,color:#2C2C2A

The hook is the load-bearing part. If the hook does not run, the injection never happens, and the 14 skills go back to being dormant suggestions the model will mostly ignore. This is why a Windows box without Git Bash gets a plugin that reports itself as enabled and does nothing.

The skills load in two stages. Only the names and short descriptions are always in context. The full body of a skill - and subagent-driven-development alone is over 500 lines - loads only when the model invokes it. That two-stage design is why the always-on cost is small and the on-invoke cost is not, which you will measure yourself in step 3.

Step 1: Install Superpowers from the official marketplace

Goal. Get the plugin onto your machine from Anthropic's marketplace.

Why this step. Superpowers is distributed through two different marketplaces, and they are not interchangeable. Anthropic's claude-plugins-official is registered automatically the first time you start Claude Code interactively, so there is nothing to add. The author also runs obra/superpowers-marketplace, which you would have to register by hand. The trap in running both is covered at the end of this step.

Code. Run this from any directory:

code
claude plugin install superpowers@claude-plugins-official

Run it. The command clones the marketplace if needed and copies the plugin into your local cache. It does not need a project directory.

Expected output. The install prints a summary ending in either Plugin is now active. or Run /reload-plugins to activate. If you already have it, the command tells you so instead of reinstalling.

/reload-plugins is a slash command typed inside a session, not a shell command. If you get that second message, either start claude, type it at the prompt and exit, or just carry on: the next session you start picks the plugin up anyway.

What just happened. Claude Code copied the plugin into ~/.claude/plugins/cache/claude-plugins-official/superpowers/6.2.0/. It copies rather than symlinks, deliberately, so a marketplace cannot change code under a running install. The path carries the version, so several versions can sit side by side; superseded ones are removed automatically after 14 days.

Both marketplaces currently serve version 6.2.0, and they do not serve identical bytes. Anthropic's entry pins a raw commit SHA, 44c9b2d, which sits one commit ahead of the v6.2.0 tag that the author's marketplace points at. Because Claude Code reads the version from the plugin's own plugin.json before anything else, both report 6.2.0, and claude plugin update therefore compares 6.2.0 against 6.2.0 and does nothing. Auto-update also differs: it is on by default for official marketplaces and off for third-party ones. If you install from both, you end up with two same-named plugins from different sources, which is a mess to unpick. Choose one.

Step 2: Confirm the plugin actually loaded

Goal. Verify the install before you trust anything built on top of it.

Why this step. An install that reported success can still be inert, most obviously on Windows, where the hook can fail silently.

Code.

code
claude plugin list

Run it. From any directory.

Expected output. Among your installed plugins you should see this block:

code
  ❯ superpowers@claude-plugins-official    Version: 6.2.0    Scope: user    Status: ✔ enabled

What just happened. User scope is the default, so every project on this machine gets the plugin until you say otherwise.

This tells you the plugin is registered. It does not tell you the hook runs or that any skill fires. Those are steps 4 and 6.

Step 3: Measure what Superpowers costs your context window

Goal. Get real token numbers for what the plugin adds to every session and what each skill costs when it fires.

Why this step. You cannot decide which skills to keep from someone else's benchmark. The Hacker News thread on the Superpowers 6 announcement (item 48739459, 196 points) is where I went looking for numbers and found none. One commenter reports that it "burnt through all my max plan (before it could start making any changes)"; another that it was "burning too many tokens to do too little". Neither attaches a token count, and I did not find one anywhere else in the thread. Nobody has published the numbers for your machine either.

Code.

code
claude plugin details superpowers

Run it. From any directory.

Expected output. The command prints an inventory and a token table. This is the real output from the machine this tutorial was written on:

code
superpowers 6.2.0  Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques  Source: superpowers@claude-plugins-officialComponent inventory  Skills (14)  brainstorming, dispatching-parallel-agents, executing-plans, finishing-a-development-branch, receiving-code-review, requesting-code-review, subagent-driven-development, systematic-debugging, test-driven-development, using-git-worktrees, using-superpowers, verification-before-completion, writing-plans, writing-skills  Agents (0)  Hooks (1)  SessionStart  (harness-only — no model context cost)  MCP servers (0)  LSP servers (0)Projected token cost  Always-on:   ~688 tok   added to every sessionPer-component (rounded)  component                       always-on  on-invoke  brainstorming                         ~70      ~3.6k  dispatching-parallel-agents           ~40      ~2.2k  executing-plans                       ~40       ~800  finishing-a-development-branch        ~40      ~2.5k  receiving-code-review                 ~80      ~2.2k  requesting-code-review                ~40        ~1k  subagent-driven-development           ~40     ~10.3k  systematic-debugging                  ~40      ~3.5k  test-driven-development               ~30      ~3.3k  using-git-worktrees                   ~70      ~2.4k  using-superpowers                     ~50      ~1.1k  verification-before-completion        ~80      ~1.2k  writing-plans                         ~30      ~2.5k  writing-skills                        ~40      ~9.7k  On-invoke cost is paid each time a skill or agent fires.  Token counts are estimates and may differ from actual usage.

Your always-on figure may differ by a few tokens, because it is computed against your active model through the token-counting API and falls back to a character estimate if that call fails.

What just happened. You now have the shape of the cost, and it is lopsided. It is also one line item in a larger context budget for agent tooling. The standing charge is about 688 tokens on every session, which is negligible. The variable charge is not: subagent-driven-development costs about 10.3k tokens each time it fires, and writing-skills about 9.7k. Two of those in one session is roughly 20k tokens of instructions before any of your code is read.

Note the Hooks (1) line, which annotates the SessionStart hook as harness-only with no model context cost. That undercounts. The hook mechanism does cost the model nothing, true. But this hook's whole job is to inject text into the model's context, and the table does not charge that text to it. You measure the injection yourself in the next step.

Step 4: See exactly what the SessionStart hook injects

Goal. Run the hook by hand and read the text it puts into every one of your sessions.

Why this step. Everything the plugin does to your agent's behaviour starts with one blob of text that nothing ever prompts you to open. Two lines and no API call put it on your screen.

To be precise about what this proves: it shows the hook script works and what it contains. It cannot prove Claude Code actually runs that script, because you are invoking it with bash from inside Git Bash, where bash is by definition present. The Windows failure mode is Claude Code failing to find bash at all, and this test cannot reproduce it. Step 6 is where you see the hook fire inside a real session.

Code. The hook is a plain script. Point an environment variable at the plugin and run it. The $HOME form below resolves correctly on macOS, Linux, and Windows under Git Bash, so the same two lines work everywhere:

code
export CLAUDE_PLUGIN_ROOT="$HOME/.claude/plugins/cache/claude-plugins-official/superpowers/6.2.0"bash "$CLAUDE_PLUGIN_ROOT/hooks/session-start"

The version is a directory name, so replace 6.2.0 with whatever claude plugin list reported in step 2. If the path does not exist, list what you actually have:

code
ls "$HOME/.claude/plugins/cache/claude-plugins-official/superpowers/"

Run it. From any directory. The script only reads files and prints to standard output.

Expected output. A single JSON object. The first 500 characters look like this:

code
{  "hookSpecificOutput": {    "hookEventName": "SessionStart",    "additionalContext": "<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**\n\n---\nname: using-superpowers\ndescription: Use when starting any conversation - establishes how to find and use skills, requiring skill invocation before ANY response including clarifying questions\n---

Measure the whole thing:

code
bash "$CLAUDE_PLUGIN_ROOT/hooks/session-start" | wc -c
code
3484

What just happened. You just read the mechanism. That 3,484-byte JSON object is handed to Claude Code, which unwraps additionalContext and places it in the model's context before your first message. Inside it is the full text of the using-superpowers skill, including the line that does the actual work:

If you think there is even a 1% chance a skill might apply to what you are doing, you ABSOLUTELY MUST invoke the skill.

Now you know why the plugin behaves the way it does. There is no enforcement anywhere in this design. There is a strongly worded instruction, injected reliably, and a model that usually complies. Step 7 shows what happens when it does not.

The script also branches on which harness is running it, emitting additional_context for Cursor and hookSpecificOutput for Claude Code, because the two read different fields. If you see the Cursor shape, you set the wrong environment variable.

Step 5: Build a detector that proves which skills fired

Goal. Write a script that reads a Claude Code transcript and reports whether the hook ran, whether the plugin loaded, and which skills fired.

Why this step. Steps 2 and 4 proved the plugin is installed and the hook works in isolation. Neither tells you what happened during an actual request. Claude Code can emit a machine-readable transcript, and the events in it answer that question directly. Writing the detector once means you can check any future run in one command instead of reading a wall of JSON. It is the smallest useful piece of observability for a Claude Code session.

Create the project directory first:

code
mkdir booklog && cd booklog && git init -q && mkdir tests fixtures

Make it a git repository now, because step 8 has Claude write and commit a design document into it.

Commands run from inside booklog up to step 10, and file paths are written with the booklog/ prefix so you always know where a file belongs. Step 11 is the one exception: it creates a sibling directory and works there, then hands you back at the start of step 12. Every step says where you are when it matters.

Code. Save this as booklog/sp_detect.py:

code
"""Report what Superpowers actually did during a headless Claude Code run.Reads a stream-json transcript (one JSON object per line) from a file or stdinand answers three questions the plugin list cannot:  1. Did the SessionStart hook run, and how much text did it inject?  2. Did Claude Code load the superpowers plugin, and at what version?  3. Which skills actually fired?Usage:    python sp_detect.py fixtures/sample-stream.jsonl    claude -p "..." --output-format stream-json --verbose < /dev/null | python sp_detect.py"""from __future__ import annotationsimport jsonimport sysfrom typing import Any, Iteratordef read_events(source: Iterator[str]) -> Iterator[dict[str, Any]]:    """Yield one parsed event per non-blank line, skipping anything unparseable.    A stream-json transcript can be truncated if the run is interrupted, so a    bad final line is expected rather than exceptional.    """    for line in source:        line = line.strip()        if not line:            continue        try:            yield json.loads(line)        except json.JSONDecodeError:            continuedef summarize(events: Iterator[dict[str, Any]]) -> dict[str, Any]:    """Collapse a transcript into the three facts we care about."""    report: dict[str, Any] = {        "hook_name": None,        "injected_bytes": 0,        "plugin_version": None,        "plugin_source": None,        "plugin_errors": [],        "skills": [],    }    for event in events:        subtype = event.get("subtype")        if subtype == "hook_response" and event.get("hook_event") != "PostToolUse":            report["hook_name"] = event.get("hook_name")            report["injected_bytes"] = len(event.get("output") or "")        elif subtype == "init":            for plugin in event.get("plugins") or []:                if plugin.get("name") == "superpowers":                    report["plugin_version"] = plugin.get("version")                    report["plugin_source"] = plugin.get("source")            report["plugin_errors"] = event.get("plugin_errors") or []        elif event.get("type") == "assistant":            for block in event.get("message", {}).get("content", []):                if block.get("type") == "tool_use" and block.get("name") == "Skill":                    skill = block.get("input", {}).get("skill")                    if skill and skill not in report["skills"]:                        report["skills"].append(skill)    return reportdef render(report: dict[str, Any]) -> str:    """Format the report as fixed-width lines, one fact per line."""    lines = []    if report["hook_name"]:        lines.append(            f"hook     {report['hook_name']:<22} injected {report['injected_bytes']} bytes"        )    else:        lines.append("hook     none                   no SessionStart hook ran")    if report["plugin_version"]:        lines.append(            f"plugin   superpowers {report['plugin_version']:<10} {report['plugin_source']}"        )    else:        lines.append("plugin   superpowers not loaded")    for error in report["plugin_errors"]:        lines.append(f"error    {error.get('plugin')}: {error.get('message')}")    for skill in report["skills"]:        lines.append(f"skill    {skill}")    if not report["skills"]:        lines.append("skill    none fired")    fired = bool(report["skills"])    loaded = report["plugin_version"] is not None    if fired:        lines.append("verdict  SKILL FIRED")    elif loaded:        lines.append("verdict  LOADED BUT NO SKILL FIRED")    else:        lines.append("verdict  SUPERPOWERS NOT ACTIVE")    return "\n".join(lines)def main(argv: list[str]) -> int:    if len(argv) > 1:        with open(argv[1], encoding="utf-8") as handle:            report = summarize(read_events(handle))    else:        report = summarize(read_events(sys.stdin))    print(render(report))    return 0 if report["skills"] else 1if __name__ == "__main__":    raise SystemExit(main(sys.argv))

Test it against a fixture rather than a live call, so you can confirm the parser works before spending anything. Save this as booklog/fixtures/sample-stream.jsonl - a trimmed five-event excerpt from a real run. Each event must be exactly one line, five lines total, with no wrapping. The detector skips unparseable lines silently, so a wrapped line shows up as a missing fact rather than an error:

code
{"type":"system","subtype":"hook_started","hook_name":"SessionStart:startup","hook_event":"SessionStart"}{"type":"system","subtype":"hook_response","hook_name":"SessionStart:startup","output":"{\n  \"hookSpecificOutput\": {\n    \"hookEventName\": \"SessionStart\",\n    \"additionalContext\": \"<EXTREMELY_IMPORTANT>\\nYou have superpowers.\\n\"\n  }\n}"}{"type":"system","subtype":"init","plugins":[{"name":"superpowers","version":"6.2.0","source":"superpowers@claude-plugins-official"}]}{"type":"assistant","parent_tool_use_id":null,"message":{"content":[{"type":"tool_use","name":"Skill","input":{"skill":"superpowers:brainstorming","args":"Small tool to track books the user reads"}}]}}{"type":"result","subtype":"success","num_turns":4}

Run it.

code
python sp_detect.py fixtures/sample-stream.jsonl

Expected output.

code
hook     SessionStart:startup   injected 144 bytesplugin   superpowers 6.2.0      superpowers@claude-plugins-officialskill    superpowers:brainstormingverdict  SKILL FIRED

The byte count is 144 because the fixture's hook output is truncated for readability. A real run reports the full size.

What just happened. You have a working detector and you proved it works without spending anything on the API. It keys off hook_response for the hook's output, init for the loaded plugins and any plugin_errors, and any assistant message carrying a tool_use block named Skill. Exit code 0 when a skill fired, 1 when none did.

Step 6: Watch a skill fire on a real request

Goal. Run Claude Code headlessly against a real request and confirm through the detector that Superpowers took over.

Why this step. Everything so far has been static inspection. This is the first step where a model reads the injected instruction and does something about it. If it does not fire here, on an open-ended greenfield request, it will not fire anywhere.

Code. Run this from inside the booklog directory:

code
claude -p "I want to build a small tool to track the books I read. Help me figure out what to build." \  --output-format stream-json --verbose \  --allowedTools "Read,Glob,Grep" \  < /dev/null > run-fired.jsonl

-p runs a single prompt without opening a session and exits when it is done. --output-format stream-json with --verbose is what produces the machine-readable transcript. --allowedTools "Read,Glob,Grep" keeps the run read-only, so a demonstration cannot modify your files. And < /dev/null closes standard input, which you need in scripts.

Never add --bare to a command like this. That flag skips discovery of hooks, skills, and plugins entirely, so Superpowers would not load and the run would prove nothing. Anthropic's headless documentation recommends --bare for scripted calls generally and says it is expected to become the default for -p in a future release, so this will need watching.

Run it. The call takes about 40 seconds.

code
python sp_detect.py run-fired.jsonl

Expected output.

code
hook     SessionStart:startup   injected 3472 bytesplugin   superpowers 6.2.0      superpowers@claude-plugins-officialskill    superpowers:brainstormingverdict  SKILL FIRED

If your verdict says LOADED BUT NO SKILL FIRED instead, nothing is broken. That is the non-determinism step 7 is about, arriving early. Run the same command again; if it never fires across three attempts, the injection is not landing and step 4 is where to look.

What just happened. The model received an open-ended request, read the injected instruction, and invoked superpowers:brainstorming before writing anything. In the transcript it appears as an ordinary tool call:

code
{"type":"tool_use","name":"Skill","input":{"skill":"superpowers:brainstorming","args":"Small tool to track books the user reads"}}

The brainstorming skill carries a hard gate instructing the model not to write any code until it has presented a design and you have approved it, so instead of guessing at a schema it starts asking questions one at a time.

The injected size reads 3,472 here against the 3,484 you measured in step 4. Both are correct: step 4 counted the raw bytes the script wrote to standard output, while this counts the JSON string as it arrives in the transcript event. They are two measurements of the same injection, not a discrepancy.

Step 7: Watch the same setup skip the skill entirely

Goal. Give the same installation a small, fully specified task and watch it decline to use the skill.

Why this step. Builder.io's trial and Schwartz's review both stop at the point where Superpowers works. Neither shows it declining. That omission matters, because the plugin's own instruction says a skill must be invoked if there is even a 1% chance it applies, and the brainstorming skill states its gate applies to every project regardless of perceived simplicity. Neither statement is reliably true in practice. If you believe them, you will build workflows on an assumption that quietly fails.

Code. The request needs a small, fully specified target, so give it one. Create booklog/greet.py:

code
import sysdef main() -> int:    print(f"hello, {sys.argv[1] if len(sys.argv) > 1 else 'world'}")    return 0if __name__ == "__main__":    raise SystemExit(main())

This file exists only to be the subject of the request. It is deliberately trivial: one function, one obvious place to add a flag. No engineer designs this before writing it.

Now the same plugin and the same directory, with a narrower request:

code
claude -p "Add a --json flag to greet.py so it can print its output as JSON." \  --output-format stream-json --verbose \  --allowedTools "Read,Glob,Grep" \  < /dev/null > run-skipped.jsonl

Run it.

code
python sp_detect.py run-skipped.jsonl

Expected output.

code
hook     SessionStart:startup   injected 3472 bytesplugin   superpowers 6.2.0      superpowers@claude-plugins-officialskill    none firedverdict  LOADED BUT NO SKILL FIRED

What just happened. The hook ran, the plugin loaded, the instruction was in context, and the model chose not to invoke any skill. It stated its reasoning in the transcript:

A tiny, fully-specified change - implementing directly rather than running a brainstorming pass on a one-flag addition.

That is a defensible engineering judgement. It is also a direct contradiction of the instruction the plugin injected, on a supported version pair. Issue #2051 describes the same thing: the model evaluates skill relevance at the start of a turn, not continuously.

Superpowers shifts probabilities. It does not enforce. Hence the exit code from your detector: 1 here, 0 in step 6. Any workflow that depends on a skill having fired should check.

On a one-flag change, skipping the skill is what you want anyway.

Step 8: Run the brainstorm-to-spec cycle in a real session

Goal. Take the full interactive path once: an open question, a design conversation, and a committed spec file.

Why this step. Steps 6 and 7 used headless mode because it produces a parseable transcript. But brainstorming is a conversation - it asks questions one at a time and requires your approval before proceeding - so headless mode can only ever capture its opening move. To see the workflow the plugin was built for, you have to sit in the session.

Code. Start Claude Code interactively in the booklog directory and give it the same open-ended request:

code
claude

Then type:

code
I want to build a small tool to track the books I read. Help me figure out what to build.

Run it. Answer the questions as they come. Keep the scope small: a reading list with a title, an author, a year, and a way to print it as JSON is enough.

Expected output. Mine named the skill, read the repository, then asked exactly one question and stopped:

Using superpowers:brainstorming to turn this into a design before any code gets written.

What's the itch this scratches? Pick whichever is closest:

  • A. Memory - "Did I already read this? When? What did I think of it?"
  • B. Stats - "How many books did I read this year? What genres/authors dominate?"
  • C. Queue - "What should I read next?"
  • D. Notes - The books are the hook, but the real value is your highlights per book.

One question, then silence until I answered. It asked three in total: the itch, what "what I thought of it" should actually record, and where the read date comes from. Only then did it propose approaches, with the cost of each attached and one recommended outright:

B. Small package with an entry point (recommended) [...] After pip install -e . you type booklog add ... from anywhere - which matters, because a memory tool you have to cd to is a memory tool you stop using.

When I approved the design it wrote the document and committed it:

code
docs/superpowers/specs/2026-08-12-booklog-design.md

157 lines, from a conversation of five turns. The date is the day you run it. The skill's terminal state is to invoke writing-plans, so expect an implementation plan offered next. Decline it and leave the session with /exit. You are going to write this feature by hand in steps 9 and 10, so that step 11 has something to compare against, and the design document stays in the repository as this step's artifact.

Your questions will be different, because the skill generates them from your answers. You have finished this step when a markdown file exists under docs/superpowers/specs/ and the model offers you an implementation plan:

code
ls docs/superpowers/specs/

I captured this as a chain of resumed headless turns rather than in the terminal UI, because that is the only way to save the transcript. It is the same conversation either way. Punctuation in the two quotes above is normalised to this site's house style; nothing else is changed.

What just happened. You produced a reviewable artifact. The design document is ordinary markdown in your repository, so you can read it, edit it, and put it through code review like anything else. Schwartz's review makes the same point about the artifact rather than the process: the design doc is "a markdown file in your repository that you can read, comment on, and edit in your own editor." Builder.io's trial put a number on the same stage, at 424 lines of spec before a line of code.

It is also the part that costs the most, and the cost is not always proportionate. Issue #2079 describes this same pipeline producing 3,061 lines across 14 commits over roughly ten hours, for a change whose real logic was a single comparison. The reporter closed it themselves after finding #2063, a merged pull request that gives brainstorming a three-path router so the ceremony scales to the size of the job.

That router is not in the version you just installed. It merged on 7 August 2026, two weeks after 6.2.0 was tagged, and 6.2.0 is still the newest release as I write this. So the fix is real, it is coming, and you do not have it. Scoping this is your job today, and step 12 is the lever.

Step 9: Write the failing test first

Goal. Add a test for behaviour that does not exist yet, and watch it fail for the right reason.

Why this step. The test-driven-development skill states an iron law: no production code without a failing test first. I am following it here for a narrower reason than the skill's own. This whole tutorial is about refusing to take a claim on trust, and a test that has never gone red is a claim nobody has checked.

Code. Create booklog/booklog.py with the code the feature will extend:

code
"""A reading list you can query from the command line."""from __future__ import annotationsBOOKS = [    {"title": "The Soul of a New Machine", "author": "Tracy Kidder", "year": 1981},    {"title": "Thinking in Systems", "author": "Donella Meadows", "year": 2008},]def format_books(books: list[dict[str, object]]) -> str:    """Render each book as one human-readable line."""    return "\n".join(f"{b['title']} by {b['author']} ({b['year']})" for b in books)

Now create booklog/tests/test_booklog.py. The first test covers what already works; the second describes the feature you have not written:

code
import jsonfrom booklog import BOOKS, format_booksdef test_format_books_renders_one_line_per_book():    output = format_books(BOOKS)    assert output.splitlines() == [        "The Soul of a New Machine by Tracy Kidder (1981)",        "Thinking in Systems by Donella Meadows (2008)",    ]def test_format_books_emits_json_when_asked():    output = format_books(BOOKS, as_json=True)    assert json.loads(output) == BOOKS

Run it. From the booklog directory:

code
python -m pytest -q

Use python -m pytest, not a bare pytest. Only the module form puts the current directory on the import path, and a bare pytest here fails with ModuleNotFoundError: No module named 'booklog'.

Expected output.

code
.F                                                                       [100%]================================== FAILURES ===================================___________________ test_format_books_emits_json_when_asked ___________________    def test_format_books_emits_json_when_asked():>       output = format_books(BOOKS, as_json=True)                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^E       TypeError: format_books() got an unexpected keyword argument 'as_json'tests\test_booklog.py:15: TypeError=========================== short test summary info ===========================FAILED tests/test_booklog.py::test_format_books_emits_json_when_asked - TypeE...1 failed, 1 passed in 0.47s

The path separator in tests\test_booklog.py:15 is a backslash on Windows and a forward slash elsewhere, and the duration varies per run.

What just happened. One passed, one failed. Good. The failure is a TypeError about an unrecognised keyword argument, not an assertion error, so the test failed because the keyword really is not there yet.

Step 10: Make the test pass and verify the claim

Goal. Write the smallest change that turns the suite green, then confirm it at the command line rather than asserting it.

Why this step. The verification-before-completion skill has one rule: evidence before claims, always. I broke that rule writing this tutorial. An earlier draft of step 7 ran a command against a greet.py that no step created, because I had lifted the prompt from a different scratch directory and never walked the whole thing start to finish. The version you read has the file, because a verification pass caught the gap. I had read that step four times and did not.

Code. Replace booklog/booklog.py in full:

code
"""A reading list you can query from the command line."""from __future__ import annotationsimport argparseimport jsonBOOKS = [    {"title": "The Soul of a New Machine", "author": "Tracy Kidder", "year": 1981},    {"title": "Thinking in Systems", "author": "Donella Meadows", "year": 2008},]def format_books(books: list[dict[str, object]], as_json: bool = False) -> str:    """Render the reading list, as JSON when as_json is set."""    if as_json:        return json.dumps(books)    return "\n".join(f"{b['title']} by {b['author']} ({b['year']})" for b in books)def main() -> int:    parser = argparse.ArgumentParser(description="Print the reading list.")    parser.add_argument("--json", action="store_true", help="print the list as JSON")    args = parser.parse_args()    print(format_books(BOOKS, as_json=args.json))    return 0if __name__ == "__main__":    raise SystemExit(main())

Run it. The suite first, then the tool both ways.

code
python -m pytest -qpython booklog.pypython booklog.py --json

Expected output.

code
..                                                                       [100%]2 passed in 0.07s
code
The Soul of a New Machine by Tracy Kidder (1981)Thinking in Systems by Donella Meadows (2008)
code
[{"title": "The Soul of a New Machine", "author": "Tracy Kidder", "year": 1981}, {"title": "Thinking in Systems", "author": "Donella Meadows", "year": 2008}]

What just happened. The suite is green. You also ran the tool twice at the command line, so you know the feature works without taking pytest's word for it. format_books gained one optional parameter and the module gained an entry point. The first test is there to prove the old behaviour survived.

Step 11: Hand the same job to the plugin and compare

Goal. Give Claude the same feature request, let the test-driven-development skill drive it, and use your detector to confirm the skill actually ran.

Why this step. You just built that feature by hand, so you have a control. Without one there is no way to say what the plugin's test-first cycle adds over your own discipline, and "it felt more thorough" is not a measurement.

Code. From inside booklog, start a sibling directory holding the pre-feature state, so the comparison is fair. Everything else in this step runs in that new directory:

code
cd ..mkdir booklog-plugin && cd booklog-plugin && mkdir tests && git init -q

Create booklog-plugin/booklog.py with the pre-feature version from step 9:

code
"""A reading list you can query from the command line."""from __future__ import annotationsBOOKS = [    {"title": "The Soul of a New Machine", "author": "Tracy Kidder", "year": 1981},    {"title": "Thinking in Systems", "author": "Donella Meadows", "year": 2008},]def format_books(books: list[dict[str, object]]) -> str:    """Render each book as one human-readable line."""    return "\n".join(f"{b['title']} by {b['author']} ({b['year']})" for b in books)

and booklog-plugin/tests/test_booklog.py with only the test that already passes:

code
from booklog import BOOKS, format_booksdef test_format_books_renders_one_line_per_book():    output = format_books(BOOKS)    assert output.splitlines() == [        "The Soul of a New Machine by Tracy Kidder (1981)",        "Thinking in Systems by Donella Meadows (2008)",    ]

Run it. Commit the starting state, then hand the job over. This run has to write files, so its allowed-tools list is wider than the read-only runs in steps 6 and 7:

code
git add -A && git commit -qm "booklog: plain text output"claude -p "Add a --json flag to booklog.py so format_books can return the list as JSON. Do it test-first." \  --output-format stream-json --verbose \  --allowedTools "Read,Glob,Grep,Write,Edit,Bash" \  < /dev/null > run-tdd.jsonlpython ../booklog/sp_detect.py run-tdd.jsonl

Expected output.

code
hook     SessionStart:startup   injected 3472 bytesplugin   superpowers 6.2.0      superpowers@claude-plugins-officialskill    superpowers:test-driven-developmentverdict  SKILL FIRED

test-driven-development was the first tool call of the run, before a single file was read.

If your verdict says LOADED BUT NO SKILL FIRED, that is step 7 again, and it stings more here because you have already paid. The prompt says "test-first" but the model still decides. Re-run it, and if it still will not fire, name the skill in the prompt directly. I have not had to do that, so I cannot tell you how reliable it is.

Now open the transcript and count what it actually did:

code
python -m pytest -qcat tests/test_booklog.py

Mine printed 4 passed, against the 2 passed you have in booklog. Yours may differ, because the skill decides how many tests to write.

What just happened. The skill drove a real red-green loop instead of writing the finished code in one pass. Mine ran pytest six times:

RunResult
11 passedthe starting state
21 failed, 1 passedred
32 passedgreen
41 errorit broke something
52 failed, 2 passedred again
64 passedgreen

Four tests, not two. It wrote the two you wrote by hand, then two more that drive main() through pytest's capsys fixture:

code
def test_main_with_json_flag_prints_json(capsys):    main(["--json"])    assert json.loads(capsys.readouterr().out) == BOOKS

To make that callable it changed the signature to main(argv: list[str] | None = None) and passed argv to parse_args. That is the refactor that makes an entry point testable at all, and it is the gap in the version you wrote in step 10: your suite imports format_books directly and never touches main(), so you could delete the --json flag from the parser entirely and both your tests would still pass.

The run cost 0.78 USD across 24 turns, for one flag. I would pay that for the main() refactor and not for the flag, which is the same trade issue #2079 describes at a larger scale: the process does not scale its effort down to match a small job.

Step 12: Turn Superpowers off for one project, not globally

Goal. Disable the plugin for one project without affecting the rest of your machine, and confirm it is off.

Why this step. Step 8 showed the process pipeline can be wildly disproportionate on small work, and step 11 put a number on it: 0.78 USD and 24 turns for one flag. The three-path router in #2063 will address this, but it is not in 6.2.0, so on the version you have you scope it yourself. A repository where you make small, well-understood changes is exactly where the full brainstorm-to-plan cycle costs more than it returns.

Before the command that works, one that does not. The documented per-skill dial is skillOverrides in settings, which controls how a skill appears in the listing Claude Code shows the model. It is a reasonable first guess, and for Superpowers it does not do the job:

code
{  "_comment": "DOES NOT WORK - shown only to rule it out. See below.",  "skillOverrides": {    "brainstorming": "off"  }}

I tried this first. Twice - once with the bare key above, once with the namespaced superpowers:brainstorming form. brainstorming fired both times. The reason follows from the mental model. skillOverrides trims the skill listing, but Superpowers does not depend on the listing - the SessionStart hook injects instructions directly into context, and those instructions name the skills. Removing an entry from the menu does not remove the note telling the model to order it.

Code. Step 11 left you in booklog-plugin, so go back first. If you skipped step 11 you are already in booklog and the cd is a harmless no-op. This matters more than it looks: --scope local writes to whichever directory you are standing in, and it reports success either way, so running it from the wrong project gives you a confident message about a disable that did not happen where you wanted it.

code
cd ../booklogclaude plugin disable superpowers@claude-plugins-official --scope local

Run it. From inside the project you want it off in.

Expected output.

code
✔ Successfully disabled plugin: superpowers (scope: local)

This writes a small settings file. Check it:

code
cat .claude/settings.local.json
code
{  "enabledPlugins": {    "superpowers@claude-plugins-official": false  }}

What just happened. You turned the plugin off for this directory only. --scope local writes to .claude/settings.local.json, which is per-project and normally git-ignored, so your teammates are unaffected. Use --scope project and the setting lands in .claude/settings.json for the whole team instead.

Confirm it with the detector. This is what you built it for. Write to a new file rather than re-running step 6 verbatim, which would overwrite the transcript in which the skill fired:

code
claude -p "I want to build a small tool to track the books I read. Help me figure out what to build." \  --output-format stream-json --verbose \  --allowedTools "Read,Glob,Grep" \  < /dev/null > run-disabled.jsonlpython sp_detect.py run-disabled.jsonl
code
hook     none                   no SessionStart hook ranplugin   superpowers not loadedskill    none firedverdict  SUPERPOWERS NOT ACTIVE

The hook itself no longer runs. You removed the injection at its source instead of suppressing its effects downstream. To switch it back on, swap disable for enable.

When it breaks

Every error string below comes from a reported issue or from the tool that prints it. The four most commonly hit have their own sections; the rest are in the table at the end.

SessionStart:startup hook error on Windows

The banner appears at the top of a session and the plugin does nothing afterwards. There are three distinct causes and they are worth telling apart, because two are fixed and one is cosmetic.

If the path in the error looks cut in half, reading SessionStart:startup hook error: 'C:\Users\UserName' is not recognized as an internal or external command, operable program or batch file., then your user profile path contains a parenthesis. cmd.exe treats ( as a grouping operator and splits the path there. Fixed in 6.2.0; on older versions, install to a path with no parentheses.

If you get a red banner but Superpowers demonstrably works, and it reads Failed with non-blocking status code: node:internal/modules/cjs/loader:1423, that value is a Node module path rather than an exit code, and the fault is in how the hook is spawned rather than in the hook itself. Cosmetic, and open at the time of writing. Confirm the injection still lands with step 6.

And if there is no error at all but nothing happens, that is the silent case from the Prerequisites: run-hook.cmd looks for Git Bash in two Program Files locations and then on PATH, and when it finds none it runs exit /b 0. Claude Code sees a clean exit with no output and reports nothing. Install Git for Windows.

Unexpected token 'session-start' in expression or statement

The full banner reads Unexpected token 'session-start' in expression or statement. followed by ParserError: UnexpectedToken. PowerShell parsed the hook command's leading quoted path as a string expression rather than a command. Superpowers 6.2.0 fixed this by declaring shell: "bash" in its hook manifest, which routes the call through Git Bash. If you are on 6.2.0 and still see it, the version you think you are running is not the version that is installed - check with claude plugin list.

Marketplace "claude-plugins-official" not found

Claude Code registers the official marketplace the first time you start it interactively, so a machine that has only ever run headless may never have done it. Add it by hand with claude plugin marketplace add anthropics/claude-plugins-official, then retry the install from step 1.

Git clone timed out after 120s

Marketplaces are git repositories, and 120 seconds is the default ceiling. On a slow or proxied network, raise it by setting CLAUDE_CODE_PLUGIN_GIT_TIMEOUT_MS=300000 in the environment before re-running the step 1 install.

Everything else

SymptomVerbatim errorRoot causeFix
Reload reports no skills0 skillsThe reload summary counts only a plugin's commands/ directory. Superpowers ships skills/ and no commands/Not a failure. Verify with claude plugin details superpowers, which reports all 14
Skills stop matching after installing many plugins(no error; skills silently stop triggering)Anthropic's skills documentation puts the skill listing on a character budget of about 1% of the context window. When it overflows, descriptions are truncated starting with least-used skills, stripping the keywords needed to matchRun /doctor, check the Skills row in /context, and raise skillListingBudgetFraction or set unused skills to name-only
Headless run stalls for three secondsWarning: no stdin data received in 3s, proceeding without it. If piping from a slow command, redirect stdin explicitly: < /dev/null to skip, or wait longer.claude -p waits for piped input that never arrivesAppend < /dev/null, as every headless command in this tutorial does
Skill fires but the tool call is refusedClaude requested permissions to write to ..., but you haven't granted it yet.--allowedTools did not include the toolExpected in steps 6 and 7, which are deliberately read-only. Add the tool when you want a run to make changes
Plugin skills never appear at all(no error)Corrupted plugin cacherm -rf ~/.claude/plugins/cache, restart Claude Code, reinstall
Step 4 cannot find the hookbash: /c/Users/you/.claude/plugins/cache/.../hooks/session-start: No such file or directoryThe version in the path is a directory name, and yours is not 6.2.0ls "$HOME/.claude/plugins/cache/claude-plugins-official/superpowers/" and use the version you actually have
wc, rm -rf, or < /dev/null not recognised'wc' is not recognized as an internal or external commandYou are in PowerShell or cmd, not Git BashOpen Git Bash and re-run. Every shell block here is POSIX
Step 6 reports LOADED BUT NO SKILL FIRED(no error; the detector is working correctly)Skill invocation is probabilistic, which is step 7's whole subjectRe-run the same command. If it never fires in three attempts, the injection is not landing - go back to step 4
claude plugin details prints no token table(inventory prints, cost section missing)An older Claude Code build without the cost projectionCheck claude --version; the table needs a recent 2.1.x

The detector covers most of these already. plugin_errors appears in the init event only when something is wrong, so it prints an error line when Claude Code reports a plugin problem and stays silent otherwise.

How Superpowers' hook, skills, and your detector connect

This is the system you now have. Compare it against the mental model near the top: that one showed how the injection reaches the model, while this shows the loop you built around it, where the transcript feeds back into a check you control.

mermaid
flowchart TD
    subgraph plugin["Superpowers 6.2.0 (user scope)"]
        HOOK["SessionStart hook<br/>3,484 bytes injected"]
        SKILLS["14 skills<br/>~688 tok always-on"]
    end

    subgraph project["booklog/ project"]
        SETTINGS[".claude/settings.local.json<br/>enabledPlugins toggle"]
        DETECT["sp_detect.py"]
        FIXTURE["fixtures/sample-stream.jsonl"]
        CODE["booklog.py + tests/"]
    end

    CLI["claude -p --output-format stream-json"]
    STREAM["run-fired.jsonl<br/>transcript"]
    VERDICT{"verdict"}
    PYTEST["pytest: 2 passed"]

    HOOK --> CLI
    SKILLS --> CLI
    SETTINGS -->|"false disables the hook"| HOOK
    CLI --> STREAM
    STREAM --> DETECT
    FIXTURE -->|"offline test"| DETECT
    DETECT --> VERDICT
    VERDICT -->|"exit 0"| FIRED["SKILL FIRED"]
    VERDICT -->|"exit 1"| NOTFIRED["LOADED BUT NO SKILL FIRED"]
    CODE --> PYTEST

    style HOOK fill:#4A90E2,color:#FFFFFF
    style SKILLS fill:#4A90E2,color:#FFFFFF
    style SETTINGS fill:#FFD93D,color:#2C2C2A
    style DETECT fill:#7B68EE,color:#FFFFFF
    style FIXTURE fill:#98D8C8,color:#2C2C2A
    style CODE fill:#98D8C8,color:#2C2C2A
    style CLI fill:#95A5A6,color:#FFFFFF
    style STREAM fill:#95A5A6,color:#FFFFFF
    style VERDICT fill:#9B59B6,color:#FFFFFF
    style FIRED fill:#6BCF7F,color:#2C2C2A
    style NOTFIRED fill:#FFA07A,color:#2C2C2A
    style PYTEST fill:#6BCF7F,color:#2C2C2A

The yellow node is the one worth remembering. The settings toggle acts on the hook, because the hook is where the behaviour originates. skillOverrides was the obvious downstream lever and it did nothing, for the reason the diagram shows: the listing is not the channel the instruction arrives through.

The complete detector script and file tree

Final file tree:

code
booklog/├── .claude/│   └── settings.local.json      # step 12, disables the plugin here├── docs/superpowers/specs/│   └── YYYY-MM-DD-booklog-design.md   # step 8, paid├── fixtures/│   └── sample-stream.jsonl      # 5-event excerpt, for offline detector testing├── tests/│   └── test_booklog.py          # 2 tests, both passing├── booklog.py                   # the reading list CLI├── greet.py                     # step 7, paid├── sp_detect.py                 # the skill-firing detector├── run-fired.jsonl              # step 6, paid (skill fired)├── run-skipped.jsonl            # step 7, paid (skill did not)└── run-disabled.jsonl           # step 12, paid (plugin off)booklog-plugin/                  # step 11, paid - the plugin-driven comparison├── tests/test_booklog.py        # your 1 starting test, plus whatever the skill added├── booklog.py└── run-tdd.jsonl

Everything marked "paid" comes from a step that calls the API. Skip those and the rest still works.

Running the steps also leaves __pycache__/ and .pytest_cache/ behind, which are generated and safe to ignore.

booklog/booklog.py and booklog/tests/test_booklog.py are listed in full in step 10 and step 9. sp_detect.py and the fixture are in step 5, greet.py is in step 7, and booklog-plugin's two starting files are in step 11. The only thing not printed here is whatever the skill wrote on your machine in step 11, which is the one file that is supposed to differ from mine.

To confirm the whole thing from a fresh shell, starting one directory above booklog:

code
cd booklogpython -m pytest -qpython booklog.py --jsonpython sp_detect.py fixtures/sample-stream.jsonl

That should print 2 passed, then the JSON list, then a SKILL FIRED verdict.

Wire the detector into CI. sp_detect.py already exits 0 when a skill fired and 1 when none did, so if you run Claude Code headlessly in a pipeline and depend on a particular skill firing, pipe the transcript through it and fail the job on a non-zero exit. That turns "we installed the plugin" into an assertion your build can check. I have not done this myself yet. Headless Claude Code works through what else changes when nobody is watching the session.

Measure the on-invoke cost you actually pay. Step 3 gives estimates. Count Skill invocations per run, multiply by the per-component figures, and compare against total_cost_usd in the result event.

Replace one skill with your own. Superpowers ships writing-skills for exactly this. If brainstorming is too heavy for your repository, the advisory-versus-enforcing trade-off is worth reading before you start, because a lighter project skill inherits the same weakness you saw in step 7: it can be skipped. Then write one that runs a shorter design pass, and use step 12 to disable the plugin wherever yours should win.

References


AI Engineering

Agentic AI

Follow for more technical deep dives on AI/ML systems, production engineering, and building real-world applications:


Get the next article by email

One email when a new piece goes up. No digest, no drip sequence.

One email per new article. Unsubscribe in one click.

Books by Ranjan Kumar

Building Real-World Agentic AI Systems with LangGraph cover

Building Real-World Agentic AI Systems

The Chat Templates Handbook cover

The Chat Templates Handbook

Comments