stdd
Spec and test driven development for AI coding agents. Enforces spec-first, one-test-at-a-time
TDD through three layers: a PreToolUse guard hook, a git pre-commit gate, and slash commands
that embed the red-green-refactor loop.
Supported agents:
- Claude Code — installed as a plugin via a marketplace
- Cursor — installed as project-local rules, commands, and hooks
Requirements
stdd targets Linux development environments with Bash, Git, and standard GNU userland
tools available. Windows is supported only through a Linux-compatible environment such
as WSL or a dev container.
Install
Claude Code
/plugin marketplace add dominik-rehse/stdd
/plugin install stdd@stdd
Then in your project:
/stdd:setup
Setup is required — it installs the pre-commit hook and scripts/run-tests.sh. Until
it runs, the plugin's SessionStart hook still mirrors the workflow rules into
.claude/rules/stdd/stdd.md so the rules are loaded from the first session; add that
directory to .gitignore if you don't want auto-generated files tracked.
Updates pick up the latest commit automatically — the plugin is unversioned, so every push
to the repo is a new release.
Cursor
Clone this repository somewhere (or vendor it as a submodule), then from your project root:
bash path/to/stdd/scripts/install.sh --cursor
The installer drops .cursor/rules/stdd.mdc, four slash commands in .cursor/commands/
(/setup, /spec, /tdd, /review), .cursor/hooks.json plus .cursor/hooks/guard.sh,
the project-local scripts/run-tests.sh, and a pre-commit hook in the Git-configured
hooks path. Re-running is safe; existing files are not overwritten unless --force is
passed to refresh installed stdd files.
Both at once
bash path/to/stdd/scripts/install.sh --both
Using your own hook manager (lefthook, husky, pre-commit, ...)
The pre-commit check lives in scripts/stdd-precommit.sh, installed unconditionally
by /stdd:setup / scripts/install.sh. The Git pre-commit hook that stdd installs
by default is a thin wrapper that delegates to that script — so any other hook
manager can call the same script directly.
Pass --hook=none to skip the Git hook and wire the script into your tool of choice:
bash path/to/stdd/scripts/install.sh --hook=none
Then, for example with lefthook:
# lefthook.yml
pre-commit:
commands:
stdd:
run: ./scripts/stdd-precommit.sh
The .stdd-off marker still works — it is honoured inside stdd-precommit.sh,
so switching hook managers does not change the off-switch behaviour. Switching
modes does not uninstall hooks that a previous run wrote: if you re-run with
--hook=none after a default install, remove .git/hooks/pre-commit by hand
to avoid running the check twice.
How it works
Three enforcement layers
| Layer | What it does | When it fires |
|---|
| PreToolUse hook | Blocks writes of new src/ files unless a spec and a test already exist | Every Write call during the session |
| Git pre-commit | Blocks commits while any test is failing | git commit |
| Slash commands | Walk you through spec creation and the TDD loop | Invoked explicitly |
The guard hook in detail
The hook gates new files in src/ only. It does not block:
- Edits to files that already exist
- Writes outside
src/
- Test files (
*.test.*, *.spec.*, *_test.*, *_spec.*) — these are the RED-phase
artifact and must be writable before the implementation file
- Any write for the feature named in
.tdd-in-progress (created and removed by
/stdd:tdd on Claude Code, or /tdd on Cursor)
To pass the gate without the marker, two prerequisites must exist:
- A spec at
docs/specs/<feature>.md
- A test file — either co-located next to the implementation file
(
src/<feature>.test.<ext>) or kept separately in tests/
Feature names map flexibly: src/my_feature.ts matches both docs/specs/my_feature.md
and docs/specs/my-feature.md.
The same scripts/guard.sh runs under both agents — Claude Code via its PreToolUse
hook with ${CLAUDE_PLUGIN_ROOT}, Cursor via its preToolUse hook with the project-local
copy at .cursor/hooks/guard.sh. Exit code 2 blocks the write in both systems.
Turning stdd off
Create an empty .stdd-off file in the project root to suspend the plugin's active
hooks. The file is gitignored — the switch is per-developer, not checked in.
touch .stdd-off # off
rm .stdd-off # back on
What the marker does, and when:
| Layer | While .stdd-off exists | After rm .stdd-off |
|---|
PreToolUse guard | exits 0 on every call (next Write already free) | re-enforces on the next Write — the marker is re-read every call |
SessionStart rules hook | removes .claude/rules/stdd/ at next session start, then exits | re-mirrors .claude/rules/stdd/stdd.md at the next session start |