Skip to content

Install

Thunderstorm ships payloads for Claude Code, Codex, and Pi. tstorm install puts one in a repository; each harness can also install it as a plugin of its own.

Terminal window
GOPRIVATE='github.com/stormlightlabs/*' go install github.com/stormlightlabs/thunderstorm/cmd/tstorm@main
cd <your repository>
tstorm install

The binary carries the workflow, so nothing has to be checked out anywhere. Claude Code is the default target; --target codex and --target pi write theirs, and --dir installs into a repository you are not standing in.

Four things happen, and --check reports all of them without writing:

  1. The payload is rendered into .claude, .codex, or .pi.
  2. The commands the workflow reserves for a person are merged into the repository’s permissions.deny.
  3. The gate is registered in the same settings file, against $CLAUDE_PROJECT_DIR/.claude/hooks/gate.sh.
  4. A .tstorm.toml is written from what you named, and its documents tree is created.

Steps 2 and 3 are Claude Code’s. Codex reads an execution policy and Pi its extension, so --target codex and --target pi skip them and say so.

Step 4 invents nothing. With no flags no config is written, because a board nobody named cannot be guessed:

Terminal window
tstorm install --board stormlightlabs/13 --documents docs/internal
tstorm install --board stormlightlabs/13 --track Tropius # a shared board

--no-settings and --no-config turn off a step whose file you keep yourself.

The install writes its own files and leaves every other file in the directory alone, so your settings file, a hook of your own and a worktrees directory beside the payload all survive it. A repository already using Claude Code needs no flag for that. .tstorm-payload records which files the payload owns and which version wrote them, and the next install replaces those and removes the ones the source stopped producing.

What stops an install is a file the payload itself writes. It names every one of them and writes nothing:

.claude holds 1 path this payload also writes:
skills/implement/SKILL.md
Run again with --replace to overwrite them, or move them aside.

--replace overwrites exactly those paths, names each one it replaced, and leaves the rest of the directory as it was. That is also how a repository carrying an older hand-copied payload moves to a rendered one: every file the new payload writes collides, the list says which, and --replace takes them over.

Terminal window
tstorm uninstall # every payload under the current directory
tstorm uninstall --check # says what it would remove

The marker says what the payload owns, so uninstall removes those files and leaves everything else, including a file of yours inside the payload directory. A directory goes only once nothing is left in it. The deny rules and the gate registrations come back out of the settings file, and the rest of that file is untouched, so a hook you registered yourself on the same event stays. --keep-settings leaves the rules in place for a repository that wants to keep the decision without the loop.

.tstorm.toml is left where it is. Your board and documents tree outlive the loop that read them.

Terminal window
tstorm update # every payload under the current directory
tstorm update --check # says what it would move, writes nothing
tstorm update --dir ../other-repo

Update finds each payload by its marker, names the version that wrote it and the one this binary carries, re-renders, and merges the settings again, so a command the workflow newly reserves reaches a repository that installed months ago. A repository working two harnesses carries two payloads, and both move. A payload installed before the marker carried a version reports an unknown one and updates anyway.

Your config is left alone. A repository’s board and documents tree are its own after the first install.

Update ends by saying when a newer tstorm has been released. That lookup is cached for a day and gives up after three seconds, and --offline skips it. tstorm update --self takes the new one: it downloads the release for your machine, checks it against the checksums.txt published beside it, and renames it over the binary you are running.

Terminal window
claude plugin marketplace add stormlightlabs/thunderstorm
claude plugin install thunderstorm@stormlightlabs

This keeps the payload in Claude Code’s own cache rather than in the repository. /plugin marketplace add and /plugin install do the same from inside a session. marketplace add also takes an HTTPS URL, an SSH URL, or a local path. Thirteen skills, ten commands and four agents arrive; claude plugin details thunderstorm lists them and what they cost a session.

Both commands take --scope project, which writes the declaration into the project’s .claude/settings.json instead of the machine’s:

Terminal window
claude plugin marketplace add stormlightlabs/thunderstorm --scope project
claude plugin install thunderstorm@stormlightlabs --scope project
{
"extraKnownMarketplaces": {
"stormlightlabs": {
"source": { "source": "github", "repo": "stormlightlabs/thunderstorm" }
}
},
"enabledPlugins": { "thunderstorm@stormlightlabs": true }
}

Commit that file and everyone working in the project gets the workflow on their next session. A directory source takes a path, and a relative one is read from the project root, which is how this repository installs the payload it renders.

The four denied commands are not in it, because no plugin mechanism carries a permission. tstorm install --no-config merges them into the same file, or add the permissions.deny block by hand. Either way, check the merge:

Terminal window
tstorm check policy

It reads the deny list from the workflow the binary carries and names every reserved command .claude/settings.json does not deny. Run it in CI as well: a command added to the workflow reaches the payload on the next update and the repository’s settings never.

Terminal window
codex plugin marketplace add stormlightlabs/thunderstorm
codex plugin add thunderstorm@stormlightlabs

Start a new session, review the plugin hook, and allow it before running the workflow. The hook blocks the four merge, review, and push command prefixes while the plugin is enabled.

Invoke a skill with its plugin-qualified name:

$thunderstorm:thunderstorm Run issue 123
$thunderstorm:implement Work issue 456

Add this to the trusted project’s .codex/config.toml:

[marketplaces.stormlightlabs]
source_type = "git"
source = "https://github.com/stormlightlabs/thunderstorm.git"
[plugins."thunderstorm@stormlightlabs"]
enabled = true

Codex loads the marketplace and enables the plugin only while it works in that project. Commit the file when every Codex user on the project should get the workflow.

Register and install the plugin with the two global commands above. Then set the user-level default in ~/.codex/config.toml:

[plugins."thunderstorm@stormlightlabs"]
enabled = false

Turn it on in each trusted project’s .codex/config.toml:

[plugins."thunderstorm@stormlightlabs"]
enabled = true

Project configuration overrides user configuration. Use /plugins to inspect the installed plugin, or remove it with codex plugin remove thunderstorm@stormlightlabs.

Install tstorm, then install the repository as a Pi package for the current user:

Terminal window
GOPRIVATE='github.com/stormlightlabs/*' go install github.com/stormlightlabs/thunderstorm/cmd/tstorm@main
pi install git:github.com/stormlightlabs/thunderstorm

For one project, run this from the project root instead:

Terminal window
pi install git:github.com/stormlightlabs/thunderstorm -l

The local command writes .pi/settings.json. Pi asks the user to trust the project before it loads the package.

Without a ref, Pi installs the default branch. To install a release instead, name its tag:

Terminal window
pi install git:github.com/stormlightlabs/thunderstorm@v0.1.0

Pi keeps a tagged install at that tag through pi update. To move to a later release, remove the package with pi remove and install the new tag.

The package loads the skills, prompts, and extension from payloads/pi. The extension supplies the package path used by the scripts and blocks the four merge and push command prefixes. tstorm dispatch starts each role in its own Pi session. When Pi runs inside tmux or Zellij, dispatch opens a new window or tab in that session so the operator can watch and control it.

tstorm render builds a payload from a workflow directory rather than from the one the binary carries, which is how thunderstorm builds the payloads it publishes and how anyone editing the loop sees the result:

Terminal window
tstorm render --target claude --out payloads/claude
tstorm render --target claude --out payloads/claude --check

--source <dir> names the workflow tree, and install --source <dir> reads one too, which is how an edit reaches a repository before it is committed.

Skills call tstorm subcommands, so the binary has to be on PATH wherever the loop runs. Pi needs it for role dispatch as well. Install it with:

Terminal window
GOPRIVATE='github.com/stormlightlabs/*' go install github.com/stormlightlabs/thunderstorm/cmd/tstorm@main

The repository is private, so neither the module proxy nor the checksum database can read it. GOPRIVATE sends Go to GitHub over git for both, and git needs your GitHub credentials, which gh auth setup-git sets up. The plugin and package installs below clone the repository the same way and need the same credentials. @v0.1.0 takes the release instead of main.

A machine with no Go toolchain takes a binary from the releases instead. Each release carries linux and macOS builds for amd64 and arm64, with checksums.txt beside them:

Terminal window
tar -xzf tstorm_0.1.0_linux_amd64.tar.gz
install -m 755 tstorm ~/.local/bin/tstorm
tstorm version

tstorm version names the tag a build descends from and the commit under it, v0.1.0+g1969674, so two builds of one tag are told apart. A release names its tag alone. Homebrew is the other route planned, through a tap in this organization, and it is the only other one.

Two of them run on their own once the payload is installed. A write is checked for the frontmatter an issue cites a document by and for the writing tells the catalogue lists, and both are reported into the session rather than refused. A git commit is checked for its shape, and that one refuses: a subject that will not read in git log --oneline cannot be fixed afterwards.

Hooks load when a session starts, so the session you install from gets none of this. Start a new one. A binary the hook cannot find lets everything through and says so once per machine, since it runs on every shell command a session makes.

Checks says what each check reads, what its exit code means, which harness runs which, and what to type where none of them runs.

Working on the loop means building what you are editing:

Terminal window
git clone https://github.com/stormlightlabs/thunderstorm
cd thunderstorm
go install ./cmd/tstorm # to $GOBIN, or ~/go/bin
tstorm version

A build from a checkout names the tag its tree descends from and the commit it was built at, and appends .dirty where that tree had uncommitted changes. go build -o ~/.local/bin/tstorm ./cmd/tstorm puts it somewhere else. TSTORM_BIN names the binary for the hooks where it is on neither PATH nor the payload’s bin/, which is how to run a build under test without installing it.

The loop is the same everywhere; what it runs against is not. Configuration covers what a repository keeps for itself. The gates AGENTS.md names are there, along with the documents tree, the deny rules, the board, and which model carries which role.