Technical Documentation: nizos/tdd-guard
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
TDD Guard
[!IMPORTANT]
TDD Guard grew into Probity: the same
TDD enforcement, now for Claude Code, Codex, and GitHub Copilot CLI, with
more reliable validation and no test reporters to set up. New projects should
start there. TDD Guard remains maintained for the projects that rely on it.
Automated Test-Driven Development enforcement for Claude Code.
TDD Guard ensures Claude Code follows Test-Driven Development principles. When your agent tries to skip tests or over-implement, TDD Guard blocks the action and explains what needs to happen instead.
Features
- Test-First Enforcement - Blocks implementation without failing tests
- Minimal Implementation - Prevents code beyond current test requirements
- Lint Integration - Enforces refactoring using your linting rules
- Customizable Rules - Adjust validation rules to match your TDD style
- Flexible Validation - Choose faster or more capable models for your needs
- Session Control - Toggle on and off mid-session
Getting Started
Requirements
- Node.js 22+
- A supported test framework (Vitest, Jest, Storybook, pytest, PHPUnit, Go, Rust, RSpec, Minitest)
Installation
Open Claude Code in your project and run:
/plugin marketplace add nizos/tdd-guard/plugin install tdd-guard@tdd-guard/tdd-guard:setup
This adds the marketplace, installs the plugin, and configures the test reporter for your project. You may need to restart your terminal session or IDE extension for the setup skill to appear. For manual installation and configuration, see the installation guide.
Configuration
- Custom instructions - Customize TDD validation rules
- Lint integration - Automated refactoring support
- Strengthening enforcement - Prevent agents from bypassing validation
- Ignore patterns - Control which files are validated
- Validation Model - Choose faster or more capable model
- All settings - Complete settings documentation
Security
TDD Guard hooks run with your user permissions. We maintain automated security scanning, dependency audits, and welcome source code review. See Claude Code's security considerations for more on hook safety.
Development
Contributing
Contributions are welcome! See the contributing guidelines to get started.
Contributors:
- Python/pytest support: @Durafen
- PHP/PHPUnit support: @wazum
- Rust/cargo support: @104hp6u
- Go support: @sQVe, @wizzomafizzo
- Storybook support: @akornmeier
- Ruby/RSpec & Minitest support: @Hiro-Chiba
Support
- Discussions - Ask questions and share ideas
- Issues - Report bugs and request features
License
Overview (`.devcontainer/README.md`)
TDD Guard Development Container
A consistent, isolated environment for developing TDD Guard.
Features
- Network isolation for security
- Automated development environment setup
- Pre-configured development tools
- Persistent command history
What's Inside
| Component | Purpose |
|---|---|
| Node.js 22 | Main CLI & Vitest/Jest/Storybook reporters |
| Python 3.11 + pipx | Pytest reporter |
| PHP 8.2 + Composer | PHPUnit reporter |
| Go 1.24 | Go reporter |
| Rust 1.89 | Rust reporter with cargo-nextest |
| Ruby 3.x + Bundler | RSpec reporter |
| .NET SDK 10 | Dotnet reporter (Microsoft Testing Platform) |
| Playwright deps | Browser automation for Storybook test-runner |
| Claude Code | AI assistance |
| Dev tools | Git, zsh, fzf, Docker, gh, vim, nano, git-delta |
Key files:
Dockerfile- Container definitiondevcontainer.json- IDE configurationscripts/- Setup and configuration scriptsinit-firewall.sh- Network securitysetup-dev-environment.sh- Main setup orchestrator
Prerequisites
Choose one of:
- Docker Desktop (macOS/Windows/Linux)
- Colima (macOS)
Quick Start
VS Code
- Install Dev Containers extension
- Open project → Click green button (bottom-left) → "Reopen in Container"
- Wait for build → Start coding
Learn more about VS Code Dev Containers →
IntelliJ IDEA
- File → Remote Development → Dev Containers → New Dev Container → From Local Project
- Select Docker/Colima connection
- Browse to
/.devcontainer/devcontainer.json - Build Container and Continue
Resources:
Tips & Troubleshooting
Colima Setup
IntelliJ requires more resources than the default:
# Basic setup
colima start --cpu 2 --memory 4
# With performance optimization
colima start --cpu 4 --memory 8 --vm-type vz --mount-type virtiofsmacOS Performance
For better file performance:
- Docker Desktop: Enable VirtioFS in Settings → General → Choose file sharing implementation
- Colima: Use
--vm-type vz --mount-type virtiofsflag when starting (see above)
Common Commands
npm test # Run all tests
npm run test:unit # Run unit tests only
npm run test:reporters # Run reporter tests
npm run checks # Run all quality checks (typecheck, lint, format, test)Additional Notes
- Command history is preserved between container sessions in a persistent volume
- Network isolation restricts outbound connections for security (see
scripts/init-firewall.shfor allowed services) - Environment variables like
NODE_OPTIONSandUSE_SYSTEM_CLAUDEare pre-configured - Timezone defaults to Europe/Stockholm but can be changed via
TZbuild arg
Overview (`plugin/README.md`)
TDD Guard Plugin
Automated Test-Driven Development enforcement for Claude Code.
When Claude Code attempts to edit or write files, TDD Guard validates that changes follow TDD principles — blocking operations that skip tests or over-implement.
Setup
After installing the plugin, run the setup skill to configure a test reporter for your project:
/tdd-guard:setupThis detects your test framework and configures the appropriate reporter.
Note: You may need to restart your terminal session or IDE extension for the setup skill to appear.
What's included
- Hooks — Automatically registered on install. Validate file operations, handle
tdd-guard on/offcommands, and manage session state. - Setup skill —
/tdd-guard:setupdetects your test framework and configures the reporter.
Requirements
- Node.js 22+
- A supported test framework
Learn more
Overview (`reporters/dotnet/README.md`)
TDD Guard .NET Reporter
Microsoft Testing Platform (MTP) V2 extension that captures test results
for TDD Guard validation. Works with any .NET test framework that supports
MTP V2 — one package covers TUnit, MSTest, xUnit, NUnit, and more.
How it works
The reporter is an MTP V2 extension that loads in-process alongside your
test framework. It subscribes to TestNodeUpdateMessage events — the
standard MTP event type that all frameworks publish when a test completes.
When a test session finishes, the extension writes results to.claude/tdd-guard/data/test.json in the TDD Guard wire format. The
TDD Guard validation hook reads this file to enforce TDD discipline.
The extension is framework-agnostic. It does not know or care which
framework is running. MTP handles the framework integration; we handle
the test result capture.
Requirements
- .NET 10+
- TDD Guard
Installation
Planned distribution: NuGet.org (published by the maintainer).
Add a PackageReference to your test project:
<ItemGroup>
<PackageReference Include="TddGuard.Dotnet" Version="*" />
</ItemGroup>For solutions with multiple test projects, add it once viaDirectory.Build.props in your tests directory:
<Project>
<ItemGroup>
<PackageReference Include="TddGuard.Dotnet" Version="*" />
</ItemGroup>
</Project>No manual hook code is needed — the package auto-registers viabuildTransitive/*.props which injects the MTP builder hook at build time.
Usage
The extension registers automatically. Running dotnet test is enough
once the package is referenced.
Configuration
Project Root
Set the TDD_GUARD_PROJECT_ROOT environment variable to your project root:
export TDD_GUARD_PROJECT_ROOT="/absolute/path/to/project/root"CLAUDE_PROJECT_DIR is used as a fallback when TDD_GUARD_PROJECT_ROOT
is not set. Claude Code sets this variable for hook commands, though it
may not be available in all execution contexts.
Rules
- Absolute and relative paths are both accepted (per ADR-009)
- Current directory must be within the configured project root
- When neither env var is set, the extension logs a diagnostic to stderr
and disables itself (per ADR-010) — it does not silently fall back to cwd
Compatibility
Verified with smoke tests in TddGuard.Dotnet.Compat.* projects:
| Framework | MTP V2 support | Status | Notes |
|---|---|---|---|
| TUnit | Native | Verified | File path as module ID |
| MSTest v4 | Native | Verified | File path as module ID |
| xUnit v3 | Native | Verified | Via xunit.v3.mtp-v2 package |
| xUnit v2 | Via adapter | Verified | Via YTest.MTP.XUnit2 (third-party, unofficial) |
| NUnit 4 | Via runner | Verified | UID as module ID (NUnit omits file paths) |
| Reqnroll | Inherited | Expected | BDD layer over MSTest/NUnit/xUnit/TUnit |
Project structure
TddGuard.Dotnet.Core/ Domain types and pure functions (no MTP dependency)
TddGuard.Dotnet/ MTP V2 extension (listener, builder, hook)
TddGuard.Dotnet.Tests/ Unit tests, property-based tests, test infrastructure
TddGuard.Dotnet.Compat.*/ Framework compatibility smoke testsCore has no dependency on MTP. It defines the domain types
(TestState, CollectedResult, TestRunOutput), the serializer,
the file writer, and the project root resolver. Dotnet depends on
Core and MTP, wiring the domain logic into the MTP extension model.
This separation means the domain logic can be tested without MTP,
and the MTP integration can change without affecting the domain.
Development
All commands run inside the devcontainer:
docker exec -w /workspace/reporters/dotnet <container> dotnet build
docker exec -w /workspace/reporters/dotnet <container> dotnet test --project TddGuard.Dotnet.TestsFind the container name via docker ps.
See TddGuard.Dotnet.Tests/README.md for testing conventions,
infrastructure, and the framework smoke test guide.
License
MIT
Overview (`reporters/go/README.md`)
TDD Guard Go Reporter
Go test reporter that captures test results for TDD Guard validation.
Requirements
- Go 1.24+
- TDD Guard installed globally
Installation
go install github.com/nizos/tdd-guard/reporters/go/cmd/tdd-guard-go@latestConfiguration
Basic Usage
Pipe go test -json output to the reporter:
go test -json ./... 2>&1 | tdd-guard-goProject Root Configuration
For projects where tests run in subdirectories, specify the project root:
go test -json ./... 2>&1 | tdd-guard-go -project-root /absolute/path/to/project/rootConfiguration Rules
- Path must be absolute when using
-project-rootflag - Current directory must be within the configured project root
- Falls back to current directory if not specified
Makefile Integration
Add to your Makefile:
test:
go test -json ./... 2>&1 | tdd-guard-go -project-root /absolute/path/to/project/rootHow It Works
The reporter acts as a filter that:
- Reads
go test -jsonoutput from stdin - Passes the output through to stdout unchanged
- Parses test results and transforms them to TDD Guard format
- Saves results to
.claude/tdd-guard/data/test.json
This design allows it to be inserted into existing test pipelines without disrupting output.
More Information
- Test results are saved to
.claude/tdd-guard/data/test.json - See TDD Guard documentation for complete setup
License
MIT
Overview (`reporters/jest/README.md`)
TDD Guard Jest Reporter
Jest reporter that captures test results for TDD Guard validation.
Requirements
- Node.js 18+
- Jest 30.0.5+
- TDD Guard installed globally
Installation
npm install --save-dev tdd-guard-jestConfiguration
Jest Configuration
Add the reporter to your jest.config.js:
module.exports = {
reporters: [
'default',
[
'tdd-guard-jest',
{
projectRoot: __dirname,
},
],
],
}Or in jest.config.ts:
import type { Config } from 'jest'
import path from 'path'
const config: Config = {
reporters: [
'default',
[
'tdd-guard-jest',
{
projectRoot: path.resolve(__dirname),
},
],
],
}
export default configWorkspace/Monorepo Configuration
For workspaces or monorepos, pass the project root path to the reporter:
// jest.config.js in project root
const path = require('path')
module.exports = {
reporters: [
'default',
[
'tdd-guard-jest',
{
projectRoot: path.resolve(__dirname),
},
],
],
}If your jest config is in a workspace subdirectory, pass the absolute path to your project root:
module.exports = {
reporters: [
'default',
[
'tdd-guard-jest',
{
projectRoot: '/Users/username/projects/my-app',
},
],
],
}More Information
- Test results are saved to
.claude/tdd-guard/data/test.json - See TDD Guard documentation for complete setup
License
MIT
Overview (`reporters/junit5/README.md`)
TDD Guard JUnit5 Reporter
JUnit5 TestExecutionListener that captures test results for TDD Guard validation.
Requirements
- JDK 17+
- JUnit 5 (Jupiter)
- TDD Guard
Installation
Planned distribution: Maven Central (published by the maintainer).
Gradle:
testImplementation("io.github.nizos:tdd-guard-junit5:0.1.0")Maven:
<dependency>
<groupId>io.github.nizos</groupId>
<artifactId>tdd-guard-junit5</artifactId>
<version>0.1.0</version>
<scope>test</scope>
</dependency>Usage
The listener registers automatically via the JUnit Platform SPI. Running ./gradlew test or mvn test is enough once the dependency is on the test classpath.
Configuration
Project Root Configuration
Set the TDD_GUARD_PROJECT_ROOT environment variable to your project root:
export TDD_GUARD_PROJECT_ROOT="/absolute/path/to/project/root"Or use a relative path, which resolves against the test runner's cwd.
Configuration Rules
- Absolute and relative paths are both accepted (per ADR-009)
- Current directory must be within the configured project root
- Errors when neither an explicit path nor the env var is configured (per ADR-010)
Development
./gradlew testMore Information
- Test results are saved to
.claude/tdd-guard/data/test.json - See TDD Guard documentation for complete setup
License
MIT
Overview (`reporters/minitest/README.md`)
TDD Guard Minitest Reporter
Minitest reporter that captures test results for TDD Guard validation.
Requirements
- Ruby 3.3+
- Minitest 5.0+
- TDD Guard
Installation
Install TDD Guard by following the instructions in the TDD Guard repository.
Add the reporter to your Gemfile:
gem "tdd-guard-minitest"Or install directly:
gem install tdd-guard-minitestUsage
Require the autorun entry point before your test files. This registers the reporter as a Minitest plugin and also installs an at_exit hook that captures load errors (for example, a new test file that requires an implementation file that doesn't exist yet) as synthetic failed tests, so TDD Guard can see that a test tried to run.
Run a single test file directly:
ruby -rtdd_guard_minitest/autorun test/my_test.rbFor Rails or Rake projects, require it from test/test_helper.rb:
require "tdd_guard_minitest/autorun"Or pass it to your Rake::TestTask so every rake test invocation loads it first:
Rake::TestTask.new do |t|
t.ruby_opts << "-rtdd_guard_minitest/autorun"
endConfiguration
Project Root Configuration
Set the TDD_GUARD_PROJECT_ROOT environment variable to your project root:
export TDD_GUARD_PROJECT_ROOT="/absolute/path/to/project/root"Configuration Rules
- Set
TDD_GUARD_PROJECT_ROOTto the project root. Absolute and relative paths are both accepted; relative paths resolve against the working directory at runtime. - Tests must be run from a directory at or below the project root.
- The reporter raises at startup when the variable is not set. It no longer silently falls back to the current directory.
Development
bundle install
bundle exec rspecMore Information
- Test results are saved to
.claude/tdd-guard/data/test.json - See TDD Guard documentation for complete setup
License
MIT
Overview (`reporters/phpunit/README.md`)
TDD Guard PHPUnit Reporter
PHPUnit reporter that captures test results for TDD Guard validation.
Requirements
- PHP 8.1+
- PHPUnit 9.0+ or 10.0+ or 11.0+ or 12.0+
- TDD Guard installed globally
Installation
composer require --dev tdd-guard/phpunitConfiguration
PHPUnit 10+ Configuration
Add the extension to your phpunit.xml:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="Application Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<extensions>
<bootstrap class="TddGuard\PHPUnit\TddGuardExtension">
<parameter name="projectRoot" value="/absolute/path/to/project/root"/>
</bootstrap>
</extensions>
</phpunit>PHPUnit 9.x Configuration
Add the listener to your phpunit.xml:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="Application Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<listeners>
<listener class="TddGuard\PHPUnit\TddGuardListener">
<arguments>
<string>/absolute/path/to/project/root</string>
</arguments>
</listener>
</listeners>
</phpunit>Project Root Configuration
Set the project root using any ONE of these methods:
Option 1: PHPUnit Configuration (Recommended)
Use the projectRoot parameter in your phpunit.xml (see examples above).
Option 2: Environment Variable
export TDD_GUARD_PROJECT_ROOT=/absolute/path/to/project/rootConfiguration Rules
- Project root must be configured via the
projectRootparameter orTDD_GUARD_PROJECT_ROOTenvironment variable - Tests must be run from somewhere within the project root directory
- Relative paths are supported but resolve against the working directory at runtime — if tests may run from different directories, use an absolute path to ensure results are always written to the correct location
More Information
- Test results are saved to
.claude/tdd-guard/data/test.json - See TDD Guard documentation for complete setup
License
MIT
Overview (`reporters/phpunit/SYNC_README.md`)
TDD Guard PHPUnit Reporter
This repository is automatically synchronized from the main TDD Guard monorepo.
Important Notice
This is a read-only mirror. Please do not submit pull requests here.
- Source code: https://github.com/nizos/tdd-guard/tree/main/reporters/phpunit
- Issues: https://github.com/nizos/tdd-guard/issues
- Pull requests: https://github.com/nizos/tdd-guard/pulls
Installation
composer require --dev tdd-guard/phpunitWhy a Separate Repository?
Packagist requires composer.json to be at the root of the repository. Since TDD Guard is a monorepo containing multiple packages (npm, Python, PHP), we maintain this synchronized copy specifically for Packagist distribution.
Synchronization
This repository is automatically updated whenever changes are pushed to the reporters/phpunit directory in the main repository.
License
MIT - See the main repository for details.
Overview (`reporters/pytest/README.md`)
TDD Guard Pytest Reporter
Pytest plugin that captures test results for TDD Guard validation.
Requirements
- Python 3.8+
- pytest 6.0+
- TDD Guard installed globally
Installation
pip install tdd-guard-pytestThe plugin activates automatically when installed.
Configuration
Project Root Configuration
Set tdd_guard_project_root to your project root using any ONE of these methods:
Option 1: pyproject.toml
[tool.pytest.ini_options]
tdd_guard_project_root = "/absolute/path/to/project/root"Option 2: pytest.ini
[pytest]
tdd_guard_project_root = /absolute/path/to/project/rootOption 3: setup.cfg
[tool:pytest]
tdd_guard_project_root = /absolute/path/to/project/rootConfiguration Rules
- Path must be absolute
- Current directory must be within the configured project root
- Falls back to current directory if configuration is invalid
Development
When developing the pytest reporter, you need to configure the project root to ensure test results are saved to the correct location:
Copy the example configuration:
bashcp pytest.ini.example pytest.iniEdit
pytest.iniand set the absolute path to your TDD Guard project root:ini[pytest] tdd_guard_project_root = /absolute/path/to/tdd-guard
Note: pytest.ini is gitignored to avoid committing machine-specific paths.
More Information
- Test results are saved to
.claude/tdd-guard/data/test.json - See TDD Guard documentation for complete setup
License
MIT
Overview (`reporters/rspec/README.md`)
TDD Guard RSpec Reporter
RSpec formatter that captures test results for TDD Guard validation.
Requirements
- Ruby 3.3+
- RSpec 3.0+
- TDD Guard
Installation
Install TDD Guard by following the instructions in the TDD Guard repository.
Add the reporter to your Gemfile:
gem "tdd-guard-rspec"Or install directly:
gem install tdd-guard-rspecUsage
Run RSpec with the TDD Guard formatter:
rspec --format TddGuardRspec::FormatterOr add it to your .rspec file:
--format TddGuardRspec::FormatterConfiguration
Project Root Configuration
Set the TDD_GUARD_PROJECT_ROOT environment variable to your project root:
export TDD_GUARD_PROJECT_ROOT="/absolute/path/to/project/root"Configuration Rules
- Set
TDD_GUARD_PROJECT_ROOTto the project root. Absolute and relative paths are both accepted; relative paths resolve against the working directory at runtime. - Tests must be run from a directory at or below the project root.
- The reporter raises at startup when the variable is not set. It no longer silently falls back to the current directory.
Development
bundle install
bundle exec rspecMore Information
- Test results are saved to
.claude/tdd-guard/data/test.json - See TDD Guard documentation for complete setup
License
MIT
Overview (`reporters/rust/README.md`)
TDD Guard Rust Reporter
Rust test reporter that captures test results for TDD Guard validation.
Note: This reporter is part of the TDD Guard project, which ensures Claude Code follows Test-Driven Development principles.
Requirements
- Rust 1.70+
- TDD Guard installed globally
cargo-nextest(recommended) orcargo testwith JSON output support
Installation
Step 1: Install TDD Guard
npm install tdd-guardStep 2: Install the Rust reporter
cargo install tdd-guard-rustUsage
The reporter works as a filter that processes test output while passing it through unchanged.
With cargo-nextest (Recommended)
cargo nextest run 2>&1 | tdd-guard-rust --project-root /absolute/path/to/projectWith cargo test
cargo test -- -Z unstable-options --format json 2>&1 | tdd-guard-rust --project-root /absolute/path/to/projectDirect Execution
The reporter can also execute tests directly:
# Auto-detect runner (prefers nextest)
tdd-guard-rust --project-root /absolute/path/to/project
# Force specific runner
tdd-guard-rust --project-root /absolute/path/to/project --runner nextest
tdd-guard-rust --project-root /absolute/path/to/project --runner cargoMakefile Integration
Add to your Makefile:
.PHONY: test
test:
cargo nextest run 2>&1 | tdd-guard-rust --project-root $(PWD) --passthrough
.PHONY: test-tdd
test-tdd:
tdd-guard on && $(MAKE) testConfiguration
Project Root
The --project-root flag must be an absolute path to your project directory. This is where the .claude/tdd-guard/data/test.json file will be written.
Flags
--passthrough: Force passthrough mode even if stdin is a terminal--runner [auto|nextest|cargo]: Choose test runner for direct execution (default: auto)--project-root: Absolute path to project directory (required)
How It Works
The reporter captures JSON-formatted test output, passes it through unchanged to stdout, and saves TDD Guard-formatted results to .claude/tdd-guard/data/test.json.
License
MIT - See LICENSE file in the repository root.
Overview (`reporters/storybook/README.md`)
TDD Guard Storybook Reporter
Storybook test-runner reporter that captures test results for TDD Guard validation.
Requirements
- Node.js 18+
- Storybook 7.x - 9.x
- @storybook/test-runner 0.19.0+
- TDD Guard installed globally
[!IMPORTANT]
For Storybook v10.x, use thetdd-guard-vitestreporter w/ Vitest
Installation
npm install --save-dev tdd-guard-storybookConfiguration
Basic Configuration
Add the reporter to your .storybook/test-runner.ts:
import { StorybookReporter } from 'tdd-guard-storybook'
const reporter = new StorybookReporter()
module.exports = {
async postVisit(page, context) {
await reporter.onStoryResult(context)
},
}
process.on('exit', () => {
reporter.onComplete()
})Workspace/Monorepo Configuration
For workspaces or monorepos, pass the project root path in the reporter options:
import { StorybookReporter } from 'tdd-guard-storybook'
import path from 'path'
const reporter = new StorybookReporter({
projectRoot: path.resolve(__dirname, '../..'),
})
module.exports = {
async postVisit(page, context) {
await reporter.onStoryResult(context)
},
}
process.on('exit', () => {
reporter.onComplete()
})If your test-runner config is in a subdirectory, specify the absolute path to your project root in the options, for example { projectRoot: '/Users/username/projects/my-app' }.
How It Works
The reporter captures results from stories that have play functions (interaction tests):
- Stories with
playfunctions are reported as tests - Stories without
playfunctions are ignored - Test results are saved to
.claude/tdd-guard/data/test.json
Output Format
The reporter writes test results in a format compatible with TDD Guard validation:
{
"testModules": [
{
"moduleId": "button--primary",
"tests": [
{
"name": "Primary",
"fullName": "Button > Primary",
"state": "passed"
}
]
}
],
"unhandledErrors": [],
"reason": "passed"
}More Information
- Test results are saved to
.claude/tdd-guard/data/test.json - See TDD Guard documentation for complete setup
License
MIT
Overview (`reporters/vitest/README.md`)
TDD Guard Vitest Reporter
Vitest reporter that captures test results for TDD Guard validation.
Requirements
- Node.js 18+
- Vitest 3.2.0+
- TDD Guard installed globally
Installation
npm install --save-dev tdd-guard-vitestConfiguration
Vitest Configuration
Add the reporter to your vitest.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
reporters: ['default', ['tdd-guard-vitest']],
},
})Workspace/Monorepo Configuration
For workspaces or monorepos, pass the project root path in the reporter options:
// vitest.config.ts in project root
import { defineConfig } from 'vitest/config'
import path from 'path'
export default defineConfig({
test: {
reporters: [
'default',
['tdd-guard-vitest', { projectRoot: path.resolve(__dirname) }],
],
},
})If your vitest config is in a workspace subdirectory, specify the absolute path to your project root in the options, for example { projectRoot: '/Users/username/projects/my-app' }.
More Information
- Test results are saved to
.claude/tdd-guard/data/test.json - See TDD Guard documentation for complete setup
License
MIT
Overview (`reporters/dotnet/TddGuard.Dotnet.Tests/README.md`)
TddGuard.Dotnet.Tests
Testing philosophy
Tests verify behaviour, not implementation. The system under test (SUT) isTddGuardListener exercised through its MTP interface — events go in, JSON
comes out. Tests never reference internal types like CollectedResult,TestRunSummariser, or TestReportSerializer directly.
This means we can refactor internals freely without breaking tests, and every
test failure points to a genuine change in observable behaviour.
Test layers
Unit tests (`TddGuardListenerTests`)
Example-based tests that verify specific, named behaviours: "writes 'passed'
reason for passing test", "ignores in-progress nodes", etc. Each test follows
strict Arrange/Act/Assert via the ListenerFixture DSL:
await ListenerFixture
.Arrange(async listener =>
{
await listener.ConsumeAsync(StubProducer(), An.Event().Named("test1").Passed(), default);
})
.Act()
.Assert(async root =>
{
await Assert.That(root.Reason()).IsEqualTo("passed");
});- Arrange feeds MTP events to the listener
- Act triggers
OnTestSessionFinishingAsync(the write) - Assert inspects the resulting JSON
These tests are fast, deterministic, and produce pinpoint failure messages.
Property-based tests (`TddGuardListenerPropertyTests`)
FsCheck-driven tests that verify universal properties across hundreds of
randomly generated event sequences: "count is preserved", "reason reflects
failures", "output is valid JSON", etc.
Properties use ListenerFixture.Execute() which returns a synchronousPipelineResult suitable for FsCheck's Prop.ForAll:
Prop.ForAll(Arb.From(genEvents), events =>
{
using var result = RunEvents(events);
return result.Output!.Value.GetProperty("reason").GetString() == expected;
});These tests explore the input space far more broadly than hand-crafted examples
and catch edge cases a person would never consider.
Supporting test files
| File | Purpose |
|---|---|
ProjectRootResolverTests |
Unit tests for env var resolution (pure function, tested directly) |
ReportFileWriterTests |
Unit tests for file I/O (temp dir, error paths, overwrite) |
DiagnosticDecoratorTests |
Unit tests for logging decorators (spy-based) |
TestReportWriterTests |
Unit tests for the write-or-skip orchestrator |
TestNodeMapperTests |
Unit tests for UID parsing (pure function) |
TddGuardBuilderTests |
Unit tests for the MTP registration composition root |
These test lower-level components directly because their contracts are stable
public APIs (file paths, env vars, JSON output). They don't test internal
wiring — that's covered by the outside-in listener tests and PBTs.
Test infrastructure
`ListenerFixture`
The test harness that manages the listener lifecycle. Provides three APIs:
Arrange(...).Act().Assert(...)— async DSL for unit testsArrange(...).Act().AssertNoOutput()— for tests expecting no JSON outputArrange(...).Act().Execute()— synchronous pipeline for PBTs, returnsPipelineResultCreate()— manual session control for tests that need multiple sessions or concurrency
`TestEventBuilder` (`An.Event()`)
Test Data Builder for MTP events. Hides MTP-specific types behind
intent-revealing methods:
An.Event().Named("test1").InFile("/src/A.cs").Passed()
An.Event().Named("test1").Failed("assertion message")
An.Event().Named("test1").FailedWithExplanation("no exception, just text")
An.Event().Named("test1").FailedBare() // no exception, no explanation
An.Event().Named("test1").Error("setup failed")
An.Event().Named("test1").Skipped()
An.Event().Named("test1").InProgress()
An.Event().Named("test1").Discovered()
An.Event().Named("test1").WithNoFilePath().Passed() // null file path`MtpStubs`
Low-level MTP stubs and the FsCheck generator (GenTaggedEvent). The generator
produces TaggedEvent records that pair an MTP message with a bool IsFailed
flag, so PBT properties can compute expected outcomes without inspecting MTP
internals.
`TestJsonAssert`
Extension methods for navigating JsonElement in assertions: root.Reason(),root.Module().Test().State(), root.Module().Test().ErrorMessage(), etc.
`TempDir`
Disposable temp directory helper for file I/O tests.
Framework compatibility smoke tests
The TddGuard.Dotnet.Compat.* projects sit alongside the test project
and prove the MTP extension works with every major .NET test framework.
Each is a standalone runnable test project with a single passing test.
| Project | Framework | Notes |
|---|---|---|
Compat.TUnit |
TUnit 0.x | Native MTP, file path as module ID |
Compat.MSTest |
MSTest v4 | Native MTP, file path as module ID |
Compat.XUnit |
xUnit v3 | Native MTP via xunit.v3.mtp-v2, file path as module ID |
Compat.XUnit2 |
xUnit v2 | Third-party adapter (YTest.MTP.XUnit2), hash as module ID |
Compat.NUnit |
NUnit 4 | MTP via NUnit runner, UID as module ID (no file path) |
How they work
Each project has three files:
.csprojtargets net10.0, references the framework package andTddGuard.Dotnetvia ProjectReference. A<TestingPlatformBuilderHook>
MSBuild item tells MTP's codegen where to find the hook class.TestingPlatformBuilderHook.csis the MTP entry point. MTP's
source generator callsAddExtensionsat startup, which delegates toTddGuardBuilder.Register(builder)to wire up the listener.SmokeTests.cshas one passing test in the framework's native syntax.
When dotnet test runs, MTP starts the test host, calls the hook,
registers our listener, the framework discovers and runs the test,
MTP publishes TestNodeUpdateMessage events, our listener collects
them, and OnTestSessionFinishingAsync writes test.json.
The extension never knows which framework is running. It subscribes toTestNodeUpdateMessage which is a generic MTP event type. The only
observable differences between frameworks are in module ID source
(file path vs UID fallback) and UID format, both handled by existing
null-checks in TestNodeMapper.
Why ProjectReference instead of NuGet
The compat projects reference TddGuard.Dotnet via ProjectReference
(local source) rather than PackageReference (NuGet). This avoids the
NuGet pack/restore cycle and firewall issues with CDN IPs. The manual<TestingPlatformBuilderHook> MSBuild item replaces what the NuGet
package's buildTransitive props would normally inject. The integration
tests (reporters/test/factories/dotnet.ts) separately test the NuGet
packaging path.
Running the smoke tests
export TDD_GUARD_PROJECT_ROOT=/workspace
cd /workspace/reporters/dotnet/TddGuard.Dotnet.Compat.MSTest
dotnet test
cat /workspace/.claude/tdd-guard/data/test.jsonRepeat for each Compat.* project. If test.json appears with"reason":"passed", the framework works.
Why outside-in
Tests are written from the perspective of the consumer, not the
implementer. The SUT (system under test) is the TddGuardListener
exercised through its MTP interface: feed it events, trigger the
session finish, read the JSON.
This matters because:
- Refactoring is free. You can restructure
Summarise, change howSerializeworks, rename internal types, or merge classes — and no
test breaks as long as the JSON output stays the same. - Tests document behaviour. Each test says what the system does
("writes 'passed' reason for passing test"), not how it does it.
New contributors read the tests to understand the contract. - Agents continue the pattern. If you are an AI agent adding tests:
useListenerFixture, useAn.Event()to build inputs, assert on
the JSON output. Do not referenceCollectedResult,TestRunOutput,Summarise, orSerializein test assertions. Those are internals.
The supporting tests (ProjectRootResolverTests, ReportFileWriterTests,
etc.) test lower-level components directly because their contracts are
stable public boundaries (env vars, file paths, JSON structure). They
follow the same principle: test through the public API, not the internals.
Running tests
All commands run inside the devcontainer:
docker exec -w /workspace/reporters/dotnet <container> dotnet test --project TddGuard.Dotnet.TestsFor coverage:
docker exec -w /workspace/reporters/dotnet <container> dotnet test --project TddGuard.Dotnet.Tests \
-- --coverage --coverage-output-format cobertura --coverage-output /tmp/coverage.xmlConfig Migration (`docs/config-migration.md`)
Configuration Migration Guide
This guide helps you migrate from the legacy MODEL_TYPE configuration to the new VALIDATION_CLIENT system.
What Changed
The old configuration system required:
MODEL_TYPEto choose betweenclaude_clioranthropic_apiUSE_SYSTEM_CLAUDE(true/false) to choose between system or local Claude binary- Complex setup for finding and configuring the Claude binary
The new system simplifies this with:
VALIDATION_CLIENTto choose betweensdk,api, orcli(deprecated)- No binary configuration needed for SDK (the default)
Migration Instructions
From Claude CLI to SDK
Old configuration:
MODEL_TYPE=claude_cli
USE_SYSTEM_CLAUDE=true # or falseNew configuration:
VALIDATION_CLIENT=sdk # Or omit entirely, as SDK is the defaultThe SDK client eliminates the need for:
- Finding the Claude binary location
- Setting
USE_SYSTEM_CLAUDE - Dealing with symlinks or PATH configuration
From Anthropic API
Old configuration:
MODEL_TYPE=anthropic_api
TDD_GUARD_ANTHROPIC_API_KEY=your-api-key-hereNew configuration:
VALIDATION_CLIENT=api
TDD_GUARD_ANTHROPIC_API_KEY=your-api-key-here # Same key variableThe API configuration remains similar, just with a clearer variable name.
Legacy CLI Client (Deprecated)
If you must continue using the CLI client (not recommended):
VALIDATION_CLIENT=cli # Deprecated - use sdk insteadYou'll still need to configure the Claude binary location as described in the CLI Binary Configuration section below.
Why CLI is Deprecated
The SDK client is easier to work with and requires less configuration for different setups and operating systems.
Deprecated Variables
| Variable | Replacement | Notes |
|---|---|---|
MODEL_TYPE |
VALIDATION_CLIENT |
Map claude_cli → sdk, anthropic_api → api |
USE_SYSTEM_CLAUDE |
None | No longer needed with SDK |
TEST_MODEL_TYPE |
None | Use consistent configuration |
Common Migration Issues
API Key Conflicts
For information about API key conflicts and billing, see the Validdation Model Configuration documentation.
CLI Binary Configuration
If you're still using the deprecated CLI client (VALIDATION_CLIENT=cli), you need to help TDD Guard find your Claude installation.
Finding Your Claude Installation
# Check system-wide installation
which claude
# Check local installation
ls ~/.claude/local/claudeConfiguration Options
Option 1: Environment Variable
If Claude is in your PATH:
USE_SYSTEM_CLAUDE=trueOption 2: Symlink
Point to your Claude installation:
# Create directory if needed
mkdir -p ~/.claude/local
# Create symlink to your Claude binary
ln -s /path/to/your/claude ~/.claude/local/claudeExample for Homebrew on macOS:
ln -s /opt/homebrew/bin/claude ~/.claude/local/claudeOption 3: Migrate Installation
Use Claude Code's built-in command:
/migrate-installerGetting Help
If you encounter issues during migration:
- Check the main configuration guide
- Review the Validdation Model Configuration
- Open an issue at github.com/nizos/tdd-guard/issues
Configuration (`docs/configuration.md`)
Configuration Guide
This guide covers the configuration options for TDD Guard.
Environment Variables
TDD Guard uses environment variables for configuration.
Create a .env file in your project root:
Note: If you're migrating from an older version using MODEL_TYPE, see the Configuration Migration Guide.
# Validation client for TDD enforcement (optional)
# Options: 'sdk' (default) or 'api'
VALIDATION_CLIENT=sdk
# Model version for validation (optional)
# Default: claude-sonnet-4-6
# See https://docs.anthropic.com/en/docs/about-claude/models/overview
TDD_GUARD_MODEL_VERSION=claude-sonnet-4-6
# Anthropic API Key
# Required when VALIDATION_CLIENT is set to 'api'
# Get your API key from https://console.anthropic.com/
TDD_GUARD_ANTHROPIC_API_KEY=your-api-key-here
# Linter type for refactoring phase support (optional)
# Options: 'eslint', 'golangci-lint' or unset (no linting)
# See docs/linting.md for detailed setup and configuration
LINTER_TYPE=eslintModel Configuration
TDD Guard supports multiple validation clients:
- SDK (default) - Uses your Claude Code subscription
- API - Separate billing for CI/CD or faster validation
- CLI (deprecated) - Legacy option, not recommended
For detailed configuration, billing information, and troubleshooting, see the Validation Model Configuration guide.
If you're using the deprecated CLI client, see the Configuration Migration Guide.
Settings File Locations
Choose where to save your settings based on your needs:
- Project settings (
.claude/settings.json) - Recommended for team consistency - Local settings (
.claude/settings.local.json) - For personal preferences - User settings (
~/.claude/settings.json) - For global configuration
Hook Configuration
If you installed TDD Guard as a plugin, hooks are configured automatically. For manual hook setup, see the installation guide.
Related:
- Quick commands - Enable
tdd-guard on/offcommands - Session management - Automatic data clearing and rule setup
- Lint integration - Automated refactoring support
- Strengthening enforcement - Prevent agents from bypassing validation
Test Reporter Configuration
Manual Installation - Setup guide for all supported reporters
JavaScript/TypeScript:
Python: See Pytest reporter configuration
PHP: See PHPUnit reporter configuration
Go: See Go reporter configuration
Rust: See Rust reporter configuration
Custom Validation Rules
See Custom Instructions to customize TDD validation rules to match your practices.
Data Storage
TDD Guard stores context data in .claude/tdd-guard/data/:
instructions.md- Your custom TDD validation rules (created automatically, never overwritten)test.json- Latest test results from your test runner (Vitest or pytest)todos.json- Current todo statemodifications.json- File modification historylint.json- ESLint results (only created when LINTER_TYPE=eslint)
This directory is created automatically and should be added to .gitignore.
Troubleshooting
Dependency Versions
Vitest
Use the latest Vitest version to ensure correct test output format for TDD Guard:
npm install --save-dev vitest@latestpytest
For Python projects, ensure you have a recent version of pytest:
pip install pytest>=7.0.0Common Issues
- TDD Guard not triggering: Check that hooks are properly configured in
.claude/settings.json - Test results not captured: Ensure
VitestReporteris added to your Vitest config - "Command not found" errors: Make sure
tdd-guardis installed globally withnpm install tdd-guard - Changes not taking effect: Restart your Claude session after modifying hooks or environment variables
Updating TDD Guard
To update to the latest version:
# Update CLI tool
npm update -g tdd-guard
# For JavaScript/TypeScript projects, update the Vitest reporter in your project
npm update tdd-guard-vitest
# For Python projects, update the pytest reporter
pip install --upgrade tdd-guard-pytestCheck your current version:
npm list -g tdd-guard
pip show tdd-guard-pytestCustom Instructions (`docs/custom-instructions.md`)
Custom TDD Instructions
Customize TDD Guard's validation rules to match your specific TDD practices.
How It Works
TDD Guard uses validation rules to enforce TDD principles. You can override these default rules by creating a custom instructions file at .claude/tdd-guard/data/instructions.md.
Automatic Setup
If you have the SessionStart hook configured, the instructions file is created automatically with default rules when:
- Starting a new Claude Code session
- Resuming a session
- Using the
/clearcommand
Your custom instructions are never overwritten - once created, the file remains under your control.
Creating Custom Instructions
- Edit
.claude/tdd-guard/data/instructions.md - Adjust or replace the default rules with your TDD requirements
- Changes take effect immediately - no restart needed
Updating to Latest Defaults
When updating TDD Guard, you may want the latest default instructions:
- Delete
.claude/tdd-guard/data/instructions.md - Trigger the SessionStart hook (start new session or use
/clear) - The latest defaults will be created automatically
Alternatively, you can manually copy the default rules from src/validation/prompts/rules.ts.
Protecting Your Instructions
Prevent agents from modifying your custom instructions by denying access to TDD Guard data. See Strengthening TDD Enforcement for details.
Tips
- Start with the default instructions and modify incrementally
- Keep rules clear and actionable for consistent validation
- Share effective customizations with the TDD Guard community in GitHub Discussions
Enforcement (`docs/enforcement.md`)
Strengthening TDD Enforcement
Ensure consistent TDD validation by preventing agents from modifying guard settings or bypassing file operation hooks.
Protect Guard Settings
Prevent agents from accessing TDD Guard's configuration and state:
{
"permissions": {
"deny": ["Read(.claude/tdd-guard/**)"]
}
}This protects your custom instructions, guard state, and test results from unintended changes.
Block File Operation Bypass
If your settings allow shell commands without approval, agents can modify files without triggering TDD validation. Block these commands to maintain enforcement:
{
"permissions": {
"deny": [
"Bash(echo:*)",
"Bash(printf:*)",
"Bash(sed:*)",
"Bash(awk:*)",
"Bash(perl:*)"
]
}
}Note: Only needed if you've configured auto-approval for shell commands. May limit some agent capabilities.
Where to Apply
Add these settings to your chosen settings file. See Settings File Locations to choose the appropriate location.
Need Help?
- Report bypass methods: GitHub Issues
- Share strategies: GitHub Discussions
Ignore Patterns (`docs/ignore-patterns.md`)
Ignore Patterns Guide
Configure TDD Guard to skip validation for specific files using glob patterns.
Why Use Ignore Patterns?
Control exactly which files TDD Guard validates. Useful for monorepos, rapid prototyping, or when different parts of your codebase need different validation rules.
Default Ignore Patterns
By default, TDD Guard ignores files with these extensions:
*.md- Markdown documentation*.txt- Text files*.log- Log files*.json- JSON configuration files*.yml/*.yaml- YAML configuration files*.xml- XML files*.html- HTML files*.css- Stylesheets*.erb- Rails ERB templates*.rst- reStructuredText documentation
Custom Ignore Patterns
You can configure custom ignore patterns by creating a config.json file in the TDD Guard data directory (.claude/tdd-guard/data/):
{
"guardEnabled": true,
"ignorePatterns": [
"*.md",
"*.css",
"*.json",
"*.yml",
"**/*.generated.ts",
"**/public/**",
"*.config.*"
]
}Note: Custom patterns replace the default patterns entirely. If you want to keep some defaults (like *.md or *.json), include them in your custom list.
Pattern Syntax
Patterns use minimatch syntax (similar to .gitignore):
*.ext- Match files with extension (e.g.,*.md)dir/**- Match all files in directory (e.g.,dist/**)**/*.ext- Match extension anywhere (e.g.,**/*.test.ts)*.{js,ts}- Match multiple extensions (e.g.,*.{yml,yaml})path/**/*.ext- Match in specific path (e.g.,src/**/*.spec.js)
Managing Patterns
Viewing Current Patterns
To see which patterns are currently active, check your config.json file:
cat .claude/tdd-guard/data/config.jsonIf no custom patterns are configured, the default patterns listed above are used.
Updating Patterns
- Create or edit
.claude/tdd-guard/data/config.json - Add your
ignorePatternsarray - The changes take effect immediately
Testing Patterns
To verify your patterns work as expected:
- Edit a file that should be ignored
- TDD Guard should skip validation immediately
Summary
Ignore patterns provide the flexibility to apply TDD validation exactly where you want it in your codebase. Start with the defaults, then customize as your project's needs evolve.
Installation (`docs/installation.md`)
Manual Installation
If you installed TDD Guard as a plugin, you don't need to do any of this manually — the plugin configures hooks automatically and the /tdd-guard:setup skill handles reporter installation and configuration.
This document is a reference for manual setup or troubleshooting.
1. Install TDD Guard
Using npm:
npm install tdd-guardOr using Homebrew:
brew install tdd-guard2. Configure Claude Code Hooks
Add the following hooks to your settings file. See Settings File Locations for guidance on which file to use.
You can also configure hooks interactively by typing /hooks in Claude Code.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit|TodoWrite",
"hooks": [
{
"type": "command",
"command": "tdd-guard"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "tdd-guard"
}
]
}
],
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "tdd-guard"
}
]
}
]
}
}3. Add Test Reporter
TDD Guard needs to capture test results from your test runner. Choose your language below:
JavaScript/TypeScript
Choose your test runner:
Vitest
Install the tdd-guard-vitest reporter in your project:
npm install --save-dev tdd-guard-vitestAdd to your vitest.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
reporters: [
'default',
['tdd-guard-vitest', { projectRoot: '/Users/username/projects/my-app' }],
],
},
})Jest
Install the tdd-guard-jest reporter in your project:
npm install --save-dev tdd-guard-jestAdd to your jest.config.ts:
import type { Config } from 'jest'
const config: Config = {
reporters: [
'default',
[
'tdd-guard-jest',
{
projectRoot: '/Users/username/projects/my-app',
},
],
],
}
export default configNote: For both Vitest and Jest, specify the project root path when your test config is not at the project root (e.g., in workspaces or monorepos). This ensures TDD Guard can find the test results. See the reporter configuration docs for more details:
Storybook
Install the tdd-guard-storybook reporter in your project:
npm install --save-dev tdd-guard-storybookAdd to your .storybook/test-runner.ts:
import { StorybookReporter } from 'tdd-guard-storybook'
import path from 'path'
const reporter = new StorybookReporter({
projectRoot: path.resolve(__dirname, '..'),
})
module.exports = {
async postVisit(page, context) {
await reporter.onStoryResult(context)
},
}
process.on('exit', () => {
reporter.onComplete()
})Note: The reporter is constructed directly in your .storybook/test-runner.ts and wired into Storybook test-runner's postVisit hook. Specify the project root path when your Storybook config is not at the project root. See the Storybook reporter configuration for more details.
Tip: For Storybook 10+ with Vite-based frameworks, consider using @storybook/addon-vitest instead. This runs Storybook tests through Vitest, allowing you to use the tdd-guard-vitest reporter for faster test execution. See Storybook with Vitest Addon for setup instructions.
Python (pytest)
Install the tdd-guard-pytest reporter:
pip install tdd-guard-pytestConfigure the project root in your pyproject.toml:
[tool.pytest.ini_options]
tdd_guard_project_root = "/Users/username/projects/my-app"Note: Specify the project root path when your tests run from a subdirectory or in a monorepo setup. This ensures TDD Guard can find the test results. See the pytest reporter configuration for alternative configuration methods (pytest.ini, setup.cfg).
PHP (PHPUnit)
Install the tdd-guard/phpunit reporter in your project:
composer require --dev tdd-guard/phpunitFor PHPUnit 9.x, add to your phpunit.xml:
<listeners>
<listener class="TddGuard\PHPUnit\TddGuardListener">
<arguments>
<string>/Users/username/projects/my-app</string>
</arguments>
</listener>
</listeners>For PHPUnit 10.x/11.x/12.x, add to your phpunit.xml:
<extensions>
<bootstrap class="TddGuard\PHPUnit\TddGuardExtension">
<parameter name="projectRoot" value="/Users/username/projects/my-app"/>
</bootstrap>
</extensions>Note: Specify the project root path when your phpunit.xml is not at the project root (e.g., in subdirectories or monorepos). This ensures TDD Guard can find the test results. The reporter saves results to .claude/tdd-guard/data/test.json.
Go
Install the tdd-guard-go reporter:
go install github.com/nizos/tdd-guard/reporters/go/cmd/tdd-guard-go@latestPipe go test -json output to the reporter:
go test -json ./... 2>&1 | tdd-guard-go -project-root /Users/username/projects/my-appFor Makefile integration:
test:
go test -json ./... 2>&1 | tdd-guard-go -project-root /Users/username/projects/my-appNote: The reporter acts as a filter that passes test output through unchanged while capturing results for TDD Guard. See the Go reporter configuration for more details.
Rust
Install the tdd-guard-rust reporter:
cargo install tdd-guard-rustUse it to capture test results from cargo test or cargo nextest:
# With nextest (recommended)
cargo nextest run 2>&1 | tdd-guard-rust --project-root /Users/username/projects/my-app --passthrough
# With cargo test
cargo test -- -Z unstable-options --format json 2>&1 | tdd-guard-rust --project-root /Users/username/projects/my-app --passthroughFor Makefile integration:
test:
cargo nextest run 2>&1 | tdd-guard-rust --project-root $(PWD) --passthroughNote: The reporter acts as a filter that passes test output through unchanged while capturing results for TDD Guard. See the Rust reporter configuration for more details.
Troubleshooting
Hooks not working
The recommended installation method is via the plugin, as
described in the README.
Non-plugin installations might be incompatible with your current Claude Code version, hooks will
fail silently or exit with errors.
If you have an existing Homebrew or npm installation, remove it and switch to the plugin
installation.
Linting (`docs/linting.md`)
Linting and Refactoring Support
TDD Guard can optionally check code quality during the refactoring phase (when tests are green).
The following linters are supported: ESLint (JavaScript/TypeScript), golangci-lint (Go), and RuboCop (Ruby).
When issues are detected, the coding agent will be prompted to fix them.
Why Use Refactoring Support?
During the TDD green phase, the coding agent may:
- Clean up implementation code
- Extract methods or constants
- Improve naming
- Remove duplication
The refactoring support helps by:
- Running ESLint automatically after file modifications
- Detecting code quality issues
- Prompting the coding agent to fix any issues found
Setup
Install a supported linter in your project:
For JavaScript/TypeScript (ESLint):
bashnpm install --save-dev eslint@latestFor Go (golangci-lint): see the golangci-lint installation guide.
For Ruby (RuboCop):
bashgem install rubocopEnable linting by setting the environment variable:
bashLINTER_TYPE=eslint # for ESLint LINTER_TYPE=golangci-lint # for golangci-lint LINTER_TYPE=rubocop # for RuboCopConfigure the PostToolUse hook
You can configure this hook either through the interactive
/hookscommand or by manually editing your settings file. See Settings File Locations to choose the appropriate location. Use the same location as your PreToolUse hook.Interactive Setup (Recommended)
- Type
/hooksin Claude Code - Select
PostToolUse - After tool execution - Choose
+ Add new matcher... - Enter:
Write|Edit|MultiEdit - Select
+ Add new hook... - Enter command:
tdd-guard - Choose where to save
Manual Configuration
Add to your chosen settings file:
json{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit|MultiEdit", "hooks": [ { "type": "command", "command": "tdd-guard" } ] } ] } }- Type
How It Works
When enabled:
- After any file modification (Edit, MultiEdit, Write)
- TDD Guard runs the configured linter on modified files
- If issues are found, the coding agent receives a notification
- The agent will then fix the identified issues
Without LINTER_TYPE set, TDD Guard skips all linting operations.
Tip: Configure ESLint with complexity rules (e.g., complexity, max-depth) and the SonarJS plugin to encourage meaningful refactoring.
These rules help identify code that could benefit from simplification during the green phase.
ESLint Configuration
For effective refactoring support, consider adding these rules to your .eslintrc.js:
module.exports = {
rules: {
complexity: ['warn', 10],
'max-depth': ['warn', 4],
'max-lines-per-function': ['warn', 50],
'max-nested-callbacks': ['warn', 3],
'max-params': ['warn', 4],
},
}Troubleshooting
ESLint Not Running
- Verify ESLint is installed:
npm list eslint - Check that
LINTER_TYPE=eslintis set in your.envfile - Ensure the PostToolUse hook is configured
- Restart your Claude session after making changes
RuboCop Not Running
- Verify RuboCop is installed:
rubocop --version - Check that
LINTER_TYPE=rubocopis set in your.envfile - Ensure the PostToolUse hook is configured
- Restart your Claude session after making changes
Quick Commands (`docs/quick-commands.md`)
TDD Guard Quick Commands
TDD Guard can be quickly enabled or disabled using simple commands in your Claude Code session.
This is particularly useful when you need to temporarily disable TDD enforcement during prototyping or exploration phases.
Usage
Simply type one of these commands in your Claude Code prompt:
tdd-guard on- Enables TDD Guard enforcementtdd-guard off- Disables TDD Guard enforcement
The commands are case-insensitive, so TDD-Guard OFF, tdd-guard off, and Tdd-Guard Off all work the same way.
Setup
To enable the quick commands feature, you need to add the UserPromptSubmit hook to your Claude Code configuration.
You can set this up either through the interactive /hooks command or by manually editing your settings file. See Settings File Locations to choose the appropriate location.
Using Interactive Setup (Recommended)
- Type
/hooksin Claude Code - Select
UserPromptSubmit - When the user submits a prompt - Select
+ Add new hook... - Enter command:
tdd-guard - Choose where to save
Manual Configuration (Alternative)
Add the following to your chosen settings file:
{
"hooks": {
"userpromptsubmit": [
{
"hooks": [
{
"type": "command",
"command": "tdd-guard"
}
]
}
]
}
}Note: Your configuration file may already have other hooks configured.
Simply add the userpromptsubmit section to your existing hooks object.
Tip: To prevent agents from modifying the TDD Guard state, see Strengthening TDD Enforcement.
Session Management (`docs/session-management.md`)
TDD Guard Session Management
The SessionStart hook manages TDD Guard's session data and ensures a clean slate for each Claude Code session.
What It Does
Clears Transient Data
- Test results from previous sessions
- Lint reports and code quality checks
- Other temporary validation data
Sets Up Validation Rules
- Creates the customizable instructions file if it doesn't exist
- Preserves your custom rules if already configured
- See Custom Instructions for details
Note: The guard's enabled/disabled state is preserved across sessions.
Setup
To enable session management, you need to add the SessionStart hook to your Claude Code configuration.
You can set this up either through the interactive /hooks command or by manually editing your settings file. See Settings File Locations to choose the appropriate location.
Using Interactive Setup (Recommended)
- Type
/hooksin Claude Code - Select
SessionStart - When a new session is started - Select
+ Add new matcher… - Enter matcher:
startup|resume|clear - Select
+ Add new hook… - Enter command:
tdd-guard - Choose where to save
Manual Configuration (Alternative)
Add the following to your chosen settings file:
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear",
"hooks": [
{
"type": "command",
"command": "tdd-guard"
}
]
}
]
}
}Note: Your configuration file may already have other hooks configured.
Simply add the SessionStart section to your existing hooks object.
How It Works
The SessionStart hook triggers when:
- Claude Code starts up (
startup) - A session is resumed (
resume) - The
/clearcommand is used (clear)
When triggered, TDD Guard clears all transient data while preserving the guard state and your custom validation rules.
Tips
- No manual intervention needed - clearing happens automatically
- To toggle the guard on/off, use the quick commands
- For debugging, check
.claude/tdd-guard/to see stored data
Storybook Vitest Addon (`docs/storybook-vitest-addon.md`)
Storybook with Vitest Addon
If you're using Storybook 10+ with a Vite-based framework, you can use @storybook/addon-vitest instead of @storybook/test-runner. This approach uses TDD Guard's existing Vitest reporter to capture Storybook test results.
Why Use the Vitest Addon?
The Vitest addon offers several advantages:
- Faster execution - Uses Vitest's browser mode instead of Playwright
- Modern tooling - Built on Vitest, which is faster and more modern than Jest
- Unified testing - Run Storybook tests alongside your other Vitest tests
- Better DX - Full Storybook Test experience with interaction, accessibility, and visual tests
- Simpler setup - Uses your existing Vitest configuration
Requirements
- Storybook 10+
- Vite-based Storybook framework (React-Vite, Vue-Vite, Svelte-Vite, etc.)
- Vitest
Setup
1. Install the Vitest Addon
npm install --save-dev @storybook/addon-vitest2. Configure Storybook
Add the addon to your .storybook/main.ts:
import type { StorybookConfig } from '@storybook/react-vite'
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-vitest'],
framework: '@storybook/react-vite',
}
export default config3. Configure Vitest Reporter
Since the Vitest addon runs your Storybook tests through Vitest, you use the tdd-guard-vitest reporter to capture results:
npm install --save-dev tdd-guard-vitestAdd to your vitest.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
reporters: [
'default',
['tdd-guard-vitest', { projectRoot: '/Users/username/projects/my-app' }],
],
},
})4. Run Tests
npm run testYour Storybook interaction tests will run alongside your regular Vitest tests, and TDD Guard will capture all results.
Comparison with Test Runner
| Feature | @storybook/test-runner | @storybook/addon-vitest |
|---|---|---|
| Test framework | Jest + Playwright | Vitest browser mode |
| Storybook version | 6.4+ | 10+ |
| Framework support | All frameworks | Vite-based only |
| TDD Guard reporter | tdd-guard-storybook | tdd-guard-vitest |
| Speed | Slower (full browser) | Faster |
When to Use Each
Use @storybook/addon-vitest when:
- You're on Storybook 10+
- You're using a Vite-based framework
- You want faster test execution
- You want to unify your testing setup
Use @storybook/test-runner when:
- You're on Storybook 6.4-9.x
- You need Webpack-based framework support
- You need full Playwright browser testing
Troubleshooting
Tests Not Running
- Verify Storybook is configured with the Vitest addon
- Check that your stories have
playfunctions (interaction tests) - Ensure
tdd-guard-vitestis in your Vitest reporters
Results Not Captured
- Verify
tdd-guard-vitestis installed and configured - Check the project root path is correct in the reporter config
- Look for
.claude/tdd-guard/data/test.jsonafter running tests
Further Reading
Validation Model (`docs/validation-model.md`)
Validation Model Configuration
TDD Guard validates changes using AI. Configure both the validation client (SDK or API) and the Claude model version.
Claude Agent SDK (Default)
The recommended approach. Uses the Claude Agent SDK to communicate with Claude directly.
VALIDATION_CLIENT=sdk # Default, can be omittedFeatures:
- Works automatically with your Claude Code installation
- Uses your Claude Code subscription (no extra charges)
- Requires Claude Code to be installed and authenticated
Important: If you have ANTHROPIC_API_KEY set in your environment, Claude Code may use it for billing instead of your subscription. To avoid unexpected charges:
# Check if API key is set
echo $ANTHROPIC_API_KEY
# Unset it if present
unset ANTHROPIC_API_KEYIf you've never created an API key, you can ignore this warning.
Anthropic API
For CI/CD environments or when you need faster validation. Requires separate billing from Claude Code.
VALIDATION_CLIENT=api
TDD_GUARD_ANTHROPIC_API_KEY=your_api_key_hereGet your API key from console.anthropic.com
Notes:
- Charges separately from your Claude Code subscription (pricing)
- We use
TDD_GUARD_ANTHROPIC_API_KEY(notANTHROPIC_API_KEY) to prevent accidental charges. If you used the regularANTHROPIC_API_KEY, Claude Code might use it for all your normal coding tasks, charging your API account instead of using your subscription.
Model Selection
Configure which Claude model to use for validation (default: claude-sonnet-4-6):
# Fastest but unreliable results
TDD_GUARD_MODEL_VERSION=claude-3-5-haiku-20241022
# Best results but slowest
TDD_GUARD_MODEL_VERSION=claude-opus-4-1See Claude model overview for available models and pricing. Note: pricing only applies to API users - SDK uses your Claude Code subscription by default. Balance model capability with custom instructions to optimize for your needs.
Migration from Legacy Configuration
If you're using the old MODEL_TYPE configuration, see the Configuration Migration Guide for detailed instructions.
Adr 001 Claude Session Subdirectory (`docs/adr/001-claude-session-subdirectory.md`)
ADR-001: Execute Claude CLI from Subdirectory for Session Management
Status
Accepted
Context
The TDD Guard validation system creates a new Claude session for each validation operation when running claude commands. This results in session list clutter and makes it difficult to track the actual development sessions.
We considered two approaches:
- Clear the context before each validation using
/clearcommand - Run the Claude CLI from a subdirectory to isolate validation sessions
The first approach has limitations:
- Cannot clear and ask a question in the same command
- Would require multiple command executions
- Still shows all sessions in the same directory listing
Decision
We will execute all Claude validation commands from a .claude subdirectory within the project root.
Implementation details:
- Create
.claudedirectory if it doesn't exist - Set the
cwdoption inexecSyncto the subdirectory path - The
.claudedirectory itself is not ignored (contains settings.json which should be tracked) - User-specific files like
.claude/settings.local.jsonshould already be in.gitignore
Consequences
Positive
- Validation sessions are isolated from development sessions
- No cluttering of the main project's session list
- Automatic trust inheritance from parent directory
- Simple implementation with minimal code changes
- No impact on validation functionality
Negative
- Creates an additional directory in the project
- Slightly increases complexity in the model client
- Sessions are less visible (though this is mostly a benefit)
Neutral
- All validation sessions will appear in the
.claudesubdirectory listing - Developers need to know to look in
.claudefor validation session history
Adr 002 Secure Claude Binary Path (`docs/adr/002-secure-claude-binary-path.md`)
ADR-002: Secure Claude Binary Path Configuration
Status
Accepted
Context
CodeQL security scanning identified a potential command injection vulnerability in ClaudeModelClient where the Claude binary path is taken from an environment variable (CLAUDE_BINARY_PATH) and interpolated into a shell command executed via execSync.
The vulnerability occurs because:
- Environment variables can be manipulated by attackers
- Shell metacharacters in the path could be interpreted, allowing arbitrary command execution
- For example, setting
CLAUDE_BINARY_PATH="claude; rm -rf /"would execute both commands
We considered several approaches:
- Use execFileSync instead of execSync - Avoids shell interpretation entirely
- Validate/sanitize the binary path - Check for allowed characters only
- Use shell-quote library - Properly escape shell metacharacters
- Boolean flag for predefined paths - Switch between hardcoded safe paths
Decision
We will use a boolean environment variable USE_LOCAL_CLAUDE to switch between two hardcoded, safe paths:
- When
USE_LOCAL_CLAUDE=true: Use$HOME/.claude/local/claude - Otherwise: Use system
claudecommand
Additionally, we will:
- Implement the path logic in
ClaudeModelClientrather thanConfigclass - Use
execFileSyncinstead ofexecSyncto prevent shell interpretation - Keep the Config class focused on just providing the boolean flag
Consequences
Positive
- Eliminates injection risk - No user-controlled input in command construction
- Simple and secure - Only two possible paths, both hardcoded
- Clear intent - Boolean flag clearly indicates local vs system Claude
- Separation of concerns - Config provides settings, ModelClient handles implementation
- Future flexibility - ModelClient can handle OS-specific paths internally
Negative
- Less flexible - Users cannot specify custom installation paths
- Requires code changes - Adding new paths requires updating the code
- Platform-specific paths - May need adjustment for different operating systems
Neutral
- Migration from
CLAUDE_BINARY_PATHtoUSE_LOCAL_CLAUDEfor existing users - Documentation needs to be updated to reflect the new configuration approach
Adr 003 Remove Configurable Data Directory (`docs/adr/003-remove-configurable-data-directory.md`)
ADR-003: Remove Configurable Data Directory
Status
Accepted
Context
A security review identified a potential path traversal vulnerability in TDD Guard where the data directory path is taken from an environment variable (TDD_DATA_DIR) and used directly for file system operations without validation.
The vulnerability occurs because:
- Environment variables can be manipulated by attackers
- Path traversal sequences (
../) in the path could escape the intended directory - For example, setting
TDD_DATA_DIR="../../../../etc"would write files to system directories - The application writes files like
test.txt,todo.json, andmodifications.jsonto this directory
We considered several approaches:
- Validate and sanitize the path - Check for
../sequences and resolve to absolute paths - Restrict to project subdirectories - Ensure the path stays within the project root
- Use a whitelist of allowed paths - Only allow specific predefined directories
- Remove the configuration entirely - Hardcode the data directory path
Decision
We will remove the TDD_DATA_DIR environment variable and hardcode the data directory path to .claude/tdd-guard/data in the Config class.
The implementation will:
- Remove
TDD_DATA_DIRfrom environment variable processing - Hardcode
dataDirto.claude/tdd-guard/datain the Config constructor - Keep the existing Config class interface unchanged for dependent code
- Remove documentation about
TDD_DATA_DIRfrom.env.example, README, and CLAUDE.md
Consequences
Positive
- Eliminates path traversal risk - No user-controlled input for file paths
- Simpler implementation - No validation or sanitization code needed
- Consistent data location - All TDD Guard data in a predictable location
- Better security posture - Follows principle of least privilege
- No breaking changes for code - Config class interface remains the same
Negative
- Less flexible - Users cannot customize where TDD Guard stores its data
- Potential disk space issues - Users cannot redirect to different drives/partitions
- Testing limitations - Integration tests cannot use isolated data directories
Neutral
- The data stored (test results, todos, modifications) is operational/temporary
- Most users likely never customized this path anyway
- Follows the same security-first approach as ADR-002
Adr 004 Monorepo Architecture (`docs/adr/004-monorepo-architecture.md`)
ADR-004: Monorepo Architecture for Multi-Language Support
Status
Accepted
Context
TDD Guard originally published a single package to both npm and PyPI, with all test framework reporters mixed together in the src directory. This created several problems:
- Language mixing - JavaScript and Python code in the same package
- Publishing complexity - Single codebase published to multiple package registries
- Package bloat - Users installed code for all languages even if using only one
- Contribution barriers - Adding new reporters required navigating the entire codebase
We considered several approaches:
- Keep monolithic structure - Continue with mixed languages in one package
- Separate repositories - Create individual repos for each reporter
- Monorepo with workspaces - Keep one repo but separate packages
Decision
We will restructure TDD Guard as a monorepo using npm workspaces, with each reporter as a separate package.
The new structure:
tdd-guard/ # Main CLI package (npm)
├── src/ # Core functionality and shared code
└── package.json
reporters/
├── vitest/ # tdd-guard-vitest package (npm)
│ └── package.json
└── pytest/ # tdd-guard-pytest package (PyPI)
└── pyproject.tomlImplementation details:
- Main package exports shared functionality (Storage, Config, contracts)
- Each reporter is a standalone package with its own version
- Vitest reporter imports shared code from 'tdd-guard' package
- Python reporter is self-contained (no JavaScript dependencies)
Consequences
Positive
- Clean separation - Each language has its own package and tooling
- Smaller packages - Users only install what they need
- Independent releases - Can update reporters without touching others
- Easier contributions - Clear boundaries for adding new reporters
Negative
- Multiple packages to maintain - More release overhead
- Build complexity - Must ensure correct build order during development
Neutral
- Users now install two packages (CLI + reporter) instead of one
- Each package has its own documentation and version number
Adr 005 Claude Project Dir Support (`docs/adr/005-claude-project-dir-support.md`)
ADR-005: Support CLAUDE_PROJECT_DIR for Consistent Data Storage
Status
Accepted
Context
TDD Guard stores data in .claude/tdd-guard/data relative to the current working directory. This creates issues when:
- Users run commands from subdirectories (e.g.,
cd src && npm test) - Claude Code executes commands from different locations within a project
- Multiple
.claudedirectories are created at different levels
Previously, users had to configure CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR in Claude Code to ensure commands always run from the project root. This required additional configuration and restricted how developers could use Claude Code.
Claude Code provides the CLAUDE_PROJECT_DIR environment variable that always points to the project root, regardless of where commands are executed. This is part of Claude Code's security best practices.
Decision
We will use CLAUDE_PROJECT_DIR when available to determine the base path for TDD Guard's data directory.
The implementation:
- Check if
CLAUDE_PROJECT_DIRis set and valid - Use it as the base path for
.claude/tdd-guard/dataif available - Fall back to current working directory if not set
- Apply security validations to prevent path traversal attacks
- Reporter-provided
projectRoottakes precedence overCLAUDE_PROJECT_DIR
Security validations include:
- Validate
CLAUDE_PROJECT_DIRis an absolute path - Prevent path traversal by checking for
..sequences - Ensure current working directory is within
CLAUDE_PROJECT_DIR
When validation fails, TDD Guard throws a descriptive error and the operation is blocked, preventing any file system access with invalid paths.
Consequences
Positive
- Consistent data location - Data is always stored at the project root
- No user configuration needed - Works automatically with Claude Code
- Better developer experience - Can run commands from any project subdirectory
- Maintains security - Path validation prevents directory traversal attacks
Negative
- Additional validation code - Security checks add complexity, but this is centralized in the Config class
Neutral
- Falls back gracefully when environment variable is not present
- Replaces the need for
CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIRconfiguration
Adr 006 Phpunit Separate Repository (`docs/adr/006-phpunit-separate-repository.md`)
ADR-006: Separate Repository for PHPUnit Reporter
Status
Accepted
Context
We received a contribution for a PHPUnit reporter that allows PHP developers to use TDD Guard with their PHPUnit test suites. However, when attempting to publish this package to Packagist (the PHP package registry), we encountered a fundamental limitation: Packagist requires the composer.json file to be at the root of the repository.
TDD Guard is organized as a monorepo containing:
- The main CLI tool (TypeScript/npm)
- Vitest reporter (TypeScript/npm)
- Pytest reporter (Python/PyPI)
- PHPUnit reporter (PHP/Packagist)
This structure works well for npm (which supports workspaces) and PyPI (which can build from subdirectories), but Packagist does not support monorepos or packages in subdirectories.
Options Considered
Move composer.json to repository root
- Would make the entire TDD Guard project appear as a PHP package
- Users would download all code (TypeScript, Python, etc.) just to get the PHPUnit reporter
- Conflicts with the project's primary identity as a CLI tool
Private Packagist subscription
- Supports monorepos but requires paid subscription
- Adds ongoing cost for an open-source project
- Creates barrier for community adoption
Manual installation instructions
- Users would need to configure Composer to use VCS repository
- Poor developer experience
- Reduces discoverability and adoption
Separate repository with automated synchronization
- Maintains single source of truth in main repository
- Provides standard Packagist installation experience
- Can be automated with GitHub Actions
Decision
We will create a separate repository (tdd-guard-phpunit) that mirrors the reporters/phpunit directory from the main repository. This mirror will be automatically synchronized using GitHub Actions whenever changes are pushed to the PHPUnit reporter in the main repository.
Implementation Plan
Initial Setup
- Create
tdd-guard-phpunitrepository - Use
git subtree splitto extract PHPUnit reporter history - Push to new repository maintaining commit history
- Submit to Packagist for PHP package distribution
- Create
Automated Synchronization
- GitHub Action triggered on pushes to
reporters/phpunit/** - Uses git subtree to maintain clean history
- Force pushes to mirror repository to ensure consistency
- Syncs relevant tags (e.g.,
phpunit-v*)
- GitHub Action triggered on pushes to
Clear Communication
- Mirror repository README clearly states it's read-only
- Directs issues and PRs to main repository
- Explains the monorepo structure and rationale
Consequences
Positive
- Standard installation:
composer require --dev tdd-guard/phpunit - Packagist compatibility: Full integration with PHP ecosystem
- Automated updates: No manual synchronization needed
- Clean history: Git subtree preserves relevant commit history
- Single source of truth: All development happens in main repository
Negative
- Additional complexity: Must maintain synchronization workflow
- Delayed updates: Packagist updates depend on GitHub Action execution
- Repository proliferation: Additional repository to manage
- Potential confusion: Contributors might submit PRs to wrong repository
Security Considerations
- Uses GitHub Personal Access Token (PAT) with minimal required permissions
- Token stored as repository secret, only accessible to repository admins
- Automated workflow reduces human error in synchronization
- Mirror repository can be made explicitly read-only if needed
Future Considerations
If Packagist adds monorepo support in the future, we could deprecate the mirror repository and publish directly from the main repository. Until then, this approach provides the best balance of maintainability and user experience.
The same pattern could be applied if we need to publish other language-specific packages that don't support monorepos (e.g., RubyGems, CPAN).
Adr 007 Golangci Lint Path Support (`docs/adr/007-golangci-lint-path-support.md`)
ADR-007: Golangci-lint Path and Working Directory Support
Status
Proposed
Context
The golangci-lint integration in TDD Guard faced two specific challenges that required architectural changes to work correctly.
Key problems encountered:
File-based linting limitations - TDD Guard's default linter interface passes absolute file paths to be linted. When golangci-lint receives individual file paths, it cannot resolve other functions and types in the same Go package, leading to false undefined variable/function errors. golangci-lint is designed to work at the package/directory level, not individual files.
Working directory dependency - golangci-lint uses the current working directory to locate the project's
go.modfile for module resolution. This TDD Guard repository is primarily TypeScript with Go test artifacts in subdirectories (test/artifacts/go/). When tests run from the project root, golangci-lint fails to find the correctgo.modfiles in the test artifact directories.
We considered several approaches:
- Add module resolution to linter interface - Make the core linter system aware of Go modules and working directories
- File-based workarounds - Continue trying to make golangci-lint work with individual files
- Directory-based linting with test isolation - Use golangci-lint's natural directory mode and isolate tests that need working directory changes
- Separate Go project structure - Move Go artifacts to a different repository
Decision
We will adapt golangci-lint to work within TDD Guard's file-based linter interface using directory-based execution and test isolation, avoiding changes to the core linter architecture.
Implementation details:
Directory-based linting:
- Extract unique directories from provided file paths using
dirname() - Pass directories to golangci-lint instead of individual files
- Use
--path-mode=absflag to support absolute path arguments - This allows golangci-lint to resolve Go packages correctly while maintaining the existing linter interface
Test isolation:
- Configure golangci-lint tests to run in Vitest's fork pool instead of threads pool
- Use
process.chdir()in tests with proper cleanup via beforeEach/afterEach hooks - This allows tests to change working directory to Go test artifacts without affecting other tests
Key changes:
// Directory-based argument building
const directories = [...new Set(filePaths.map((file) => dirname(file)))]
const args = [
'run',
'--output.json.path=stdout',
'--path-mode=abs',
...directories,
]
// Simplified issue parsing (no path resolution needed)
const issues = results.flatMap(toIssue)Rationale: This approach was chosen because the situation (non-Go project testing Go linters) is rare and doesn't warrant adding brittle module resolution logic to the core linter system. The isolated test approach keeps the complexity contained to where it's needed.
Consequences
Positive
- Go language linter support - TDD Guard now supports the most popular Go linting solution
- Proper package resolution - Directory-based linting allows golangci-lint to resolve Go types and functions correctly
- Alignment with tooling - Uses golangci-lint as intended (package-level analysis)
- Comprehensive validation - Go developers get the same TDD enforcement as other languages
- Maintainable architecture - Avoided adding Go-specific complexity to the core linter interface
Negative
- Test complexity - Requires separate Vitest project configuration and fork pool for isolation
- Different execution model - Golangci-lint behaves differently than file-based linters (ESLint)
- Working directory sensitivity - Tests must manage working directory changes carefully
Neutral
- Directory-based results - May report issues in files not explicitly requested (entire package)
Security Considerations
The golangci-lint integration performs path manipulation by extracting directories from file paths using dirname() and passing them as command line arguments.
Adr 008 Storybook Reporter Design (`docs/adr/008-storybook-reporter-design.md`)
ADR-008: Storybook Reporter Design
Status
Accepted
Context
Storybook interaction tests (stories with play functions) provide valuable component test coverage, but TDD Guard didn't recognize these test failures as valid RED-phase evidence. This prevented developers from using TDD workflows with Storybook for component testing.
We needed to design a reporter that:
- Captures test results from Storybook's
@storybook/test-runner - Writes results in TDD Guard's standard format
- Handles the unique characteristics of Storybook's test model
- Follows the established patterns from other reporters (Vitest, Jest)
We considered several design decisions:
- Test granularity - Should we report all stories or only those with tests?
- Module hierarchy - How should we group stories into modules?
- Reporter lifecycle - Should we write on each story or accumulate results?
- Render failures - How should we handle stories that crash during render?
- Data extraction - Should we query the page or use provided context?
Decision
We will implement tdd-guard-storybook with the following design:
Test Granularity
Only report stories with play functions as tests.
Stories without play functions are just renders and don't provide meaningful RED/GREEN signals. This aligns with how other reporters work - they don't report non-test files. TDD Guard requires actual test failures as evidence.
Module/Test Hierarchy
One module per story file, multiple stories become tests within that module.
Structure:
{
"testModules": [
{
"moduleId": "src/Button.stories.tsx",
"tests": [
{
"name": "Primary",
"fullName": "Button > Primary",
"state": "passed"
}
]
}
]
}This matches how Vitest/Jest group tests by file. One source file = one module is intuitive and simpler to implement.
Reporter Lifecycle
Accumulate results during postVisit, write once on onExit.
API:
async postVisit(page, context) {
await reporter.onStoryResult(context)
}
async onExit() {
await reporter.onComplete()
}This matches the Vitest/Jest pattern, is more efficient (one write vs N writes), provides cleaner separation of concerns, and better handles interruptions.
Render Failure Handling
Create synthetic failed test for stories that fail to render.
A story with a play function that crashes during render is still a failing test. This matches VitestReporter pattern for module load failures and provides visibility into all failures, not just interaction test failures.
Data Extraction Strategy
Extract minimal data from test-runner context directly, no additional page queries.
Start simple with what test-runner provides out of the box. Less complexity = fewer failure points. Can enhance later if needed.
Interruption Handling
Best-effort save on exit with "interrupted" status.
Approach:
- Register process exit handler to catch interruptions
- Write accumulated results when possible
- Mark overall status as "interrupted"
- Partial results better than no results
This matches the Vitest/Jest pattern and TDD Guard already handles "interrupted" status.
Consequences
Positive
- Consistent patterns - Follows established Vitest/Jest reporter patterns
- Efficient - Single write operation instead of multiple
- Complete coverage - Captures both interaction test failures and render failures
- Simple API - Easy to configure with minimal code
- Robust - Handles interruptions gracefully
- TDD-aligned - Only reports actual tests, not passive renders
Negative
- Stories without
playnot tracked - But these aren't tests, so this is intentional - Accumulates in memory - Could be an issue for extremely large test suites, but matches other reporters
- Storybook-specific - Requires
@storybook/test-runneras peer dependency
Neutral
- Users need to configure
.storybook/test-runner.tswith the reporter - Follows same security validations as other reporters (absolute paths, project root validation)
- Results saved to standard location:
.claude/tdd-guard/data/test.json
Implementation Details
Integration Test Architecture
The Storybook reporter integration tests follow the same real-execution pattern as Jest, Vitest, PHPUnit, Pytest, Go, and Rust reporters.
Test Flow:
Integration Test
↓
Factory (storybook.ts)
↓
Copy story files → Write configs → Spawn test-runner
↓
@storybook/test-runner (Jest + Playwright)
↓
Executes stories → Calls StorybookReporter hooks
↓
StorybookReporter.onStoryResult() → Save to FileStorage
↓
Factory reads results → Returns to test assertionsTest Artifacts Structure
reporters/test/storybook/
├── .storybook/
│ └── main.js # Storybook configuration
├── Calculator.js # Simple component module
├── single-passing.stories.js # Story with passing assertions
├── single-failing.stories.js # Story with failing expect()
└── single-import-error.stories.js # Story importing non-existent moduleStories use JavaScript (.stories.js) to match the repository's test file convention (.test.js) and reduce complexity.
Factory Implementation
The reporters/test/factories/storybook.ts factory:
- Copies test artifacts - Uses existing
copyTestArtifactshelper - Generates test-runner config - Writes
test-runner-jest.config.jswith reporter - Starts Storybook dev server - Spawns
storybook devon dynamically allocated port viaget-port - Executes test-runner - Spawns
test-storybookcommand viaspawnSync - Captures results - Reporter saves to FileStorage, factory reads back
Key Design Decisions from Implementation
Test-runner integration: Uses @storybook/test-runner which integrates with Jest's reporter API through postVisit and onExit hooks. The reporter accumulates results during story execution and writes once on completion.
Async server startup: Factory implements timeout-based Storybook server detection by monitoring stdout for "Local:" or port-specific URLs. 240-second timeout accommodates slower container environments.
Optional reporter execution: Integration tests use Promise.allSettled to make reporters optional, allowing tests to run even when some language-specific reporters aren't installed.
Error handling: The reporter captures both interaction test failures and render failures (import errors, component crashes) as synthetic failed tests, ensuring complete coverage of all failure modes.
Adr 009 Relax Reporter Project Root Validation (`docs/adr/009-relax-reporter-project-root-validation.md`)
ADR-009: Relax Reporter Project Root Path Validation
Status
Accepted
Context
TDD Guard reporters write test results to .claude/tdd-guard/data/test.json under a configured project root. The project root setting exists because test runners are often invoked from a subdirectory — directly by developers, by Claude Code, and especially in monorepo and workspace setups. Without it, results land relative to the runner's cwd and the validation hook does not find them.
Reporters currently require this path to be absolute, and the PHPUnit reporter additionally rejects any path containing ... These checks were added early in the project's life, when the agentic-tool ecosystem was still young and there was little established guidance on where defensive layers belonged. Erring on the side of stricter validation was reasonable in that moment.
The ecosystem has since matured. Claude Code and comparable harnesses now provide first-class permission systems and sandboxing; containerized development environments such as devcontainers are a standard isolation pattern; OS-level isolation tooling is widely documented and available. Users running agents on sensitive work are expected to combine these layers as defense-in-depth. Concerns about a tool process writing outside its project are addressed there — at the harness, the container, and the OS — rather than at the application's input validation. TDD Guard's string-level checks on the project root have become atypical for tools at this layer, and the extra caution they represent is now out of step with where these concerns are expected to live.
That extra caution has a real cost in everyday use:
- Shared repositories — paths differ between developer machines, so a checked-in absolute path only works for one developer.
- Docker — host and container paths do not match.
Issue #139 surfaced this concretely for PHPUnit in a shared/Docker context.
This ADR is reporter-side only. The strict validation of CLAUDE_PROJECT_DIR in the main tdd-guard CLI/hook (ADR-005) is a distinct concern from the user-facing reporter configuration this ADR addresses, and remains unchanged.
Decision
Reporters will accept relative paths and paths containing .. for their project root configuration:
- Drop the absolute-path requirement. Relative paths are resolved against the reporter process's current working directory using the language's standard path resolution.
- Drop the
..rejection (currently only enforced by PHPUnit). - Resolve before validating. Any remaining checks operate on the resolved absolute path, not on the user-provided string.
This ADR does not change the hardcoded data directory subpath (.claude/tdd-guard/data, per ADR-003) or the validation of CLAUDE_PROJECT_DIR in the CLI/hook (per ADR-005).
Out of Scope
The following are deliberately deferred to follow-up work:
- Environment variable fallback (
TDD_GUARD_PROJECT_ROOT) for reporters that do not yet support it. - Whether to keep, modify, or remove the cwd-within-root sanity check — left untouched by this ADR and revisited separately.
- Loud-vs-silent failure behavior — pytest, RSpec, and Minitest currently fall back silently when validation fails; whether to make this loud is a separate question.
Consequences
Positive
- Unblocks shared repositories and Docker — relative paths checked into the repo work regardless of where it is cloned or mounted.
- Reduces configuration friction — no per-developer overrides or gitignored entries needed for the project root setting alone.
Negative
- Loses the absolute-path requirement as a misconfiguration signal. A user who passes an unintended path (e.g. a typo) will see results written there silently rather than getting an immediate format error from the reporter.
Neutral
- The hardcoded data directory remains. Reporters still write to
.claude/tdd-guard/dataunder the configured project root, with no user-controlled subpath.
Security Considerations
The hardcoded data directory subpath (per ADR-003) keeps the blast radius narrow: reporters write a single fixed file (.claude/tdd-guard/data/test.json), so even if an attacker steers the configured project root, the only thing they can affect is that single file under the chosen directory — they cannot choose a different filename or write to multiple paths. Broader containment of untrusted code execution is handled at the harness, container, and OS layers, where the appropriate primitives now exist.
Adr 010 Standardize Project Root Env Var (`docs/adr/010-standardize-project-root-env-var.md`)
ADR-010: Standardize TDD_GUARD_PROJECT_ROOT Across All Reporters
Status
Accepted
Context
TDD Guard reporters write test results to .claude/tdd-guard/data/test.json under a configured project root. The project root must point to a stable location regardless of where the test runner is invoked — otherwise, results land in the wrong directory and the validation hook does not find them.
ADR-009 relaxed the requirement that the project root be an absolute path. This unblocked shared repositories and Docker setups, but relative paths resolve against the process cwd at runtime. When an agent runs tests from a subdirectory, a relative path resolves to the wrong directory.
Not all configuration formats can compute paths at load time. An environment variable sidesteps this limitation — the value is set once per machine or environment and always produces the correct path. Some reporters already support TDD_GUARD_PROJECT_ROOT, but the coverage is incomplete.
Defaulting to cwd when no project root is configured is problematic — it silently writes results to the wrong location when tests run from a subdirectory, which is the exact scenario the project root setting was designed to prevent.
Decision
All reporters will support TDD_GUARD_PROJECT_ROOT as an environment variable for configuring the project root. The precedence chain is:
- Explicit option (CLI flag, config file parameter, constructor option) takes highest priority.
TDD_GUARD_PROJECT_ROOTenvironment variable is used when no explicit option is provided.- Error when neither is configured. Reporters will not silently default to cwd.
Removing the silent cwd fallback is a breaking change. This is intentional — silent misconfiguration is worse than an upfront error asking the user to set the project root.
Claude Code sets CLAUDE_PROJECT_DIR for hook commands, but this variable is not currently available to Bash subprocesses where reporters run. If it becomes available in the future, reporters can read it as an additional fallback between TDD_GUARD_PROJECT_ROOT and the error.
This ADR does not change the hardcoded data directory subpath (.claude/tdd-guard/data, per ADR-003) or the validation of CLAUDE_PROJECT_DIR in the CLI/hook (per ADR-005).
Out of Scope
- Whether to keep, modify, or remove the cwd-within-root sanity check — deferred per ADR-009.
- Integration with
CLAUDE_PROJECT_DIR— dependent on Claude Code exposing it to Bash subprocesses.
Consequences
Positive
- Consistent configuration across all reporters. One env var works for every language and framework, regardless of config format.
- Per-machine configuration without repo changes. Docker, shared repos, and CI each set the env var for their environment.
- No silent misconfiguration. Removing the cwd fallback prevents test results from landing in the wrong directory.
Negative
- Breaking change for zero-config users. Users who relied on the cwd default will need to set
TDD_GUARD_PROJECT_ROOTor pass an explicit option.
Neutral
- Existing env var and option users are unaffected. The explicit option still takes precedence, and reporters that already read
TDD_GUARD_PROJECT_ROOTsee no change in behavior.
--- METRICS ---
- Files Extracted: 37
- Estimated Token Budget: ~27964 tokens
- Canonical Reference: https://codewiki.google/github.com/nizos/tdd-guard