An AI-powered code review tool that compiles review context from git diffs and task summaries, then delegates to a configurable LLM subagent to produce structured review verdicts. Designed to be called by AI coding agents via MCP, used from the command line, or configured through a web UI.
- MCP server -- expose a
review_codetool that any MCP-compatible agent (Claude Code, OpenCode, Cursor, etc.) can invoke - CLI -- run one-off reviews from the terminal
- Web UI -- manage review rules, configuration, prompt templates, and browse review history
- REST API -- full CRUD API backing the web UI, also usable standalone
- Smart chunking -- large diffs are split into file-grouped chunks, reviewed individually, then synthesized into a single verdict
- Configurable rules -- maintain a set of review guidelines in SQLite, injected into every review prompt
- Editable prompt -- the review prompt lives in
prompts/code-review.mdand can be tuned without touching code - Multi-provider -- supports Anthropic (Claude) and OpenAI (GPT) as review providers
- Bun v1.0+
- An API key for Anthropic or OpenAI
# Global install
bun add -g intrusive-thoughts
# Or run directly without installing
bunx intrusive-thoughts review --summary "..."git clone https://github.com/your-org/intrusive-thoughts.git
cd intrusive-thoughts
bun install# For Anthropic (default provider)
export ANTHROPIC_API_KEY="sk-ant-..."
# For OpenAI
export OPENAI_API_KEY="sk-..."intrusive-thoughts review --summary "Added user authentication with JWT tokens"
# Or from source:
bun run src/index.ts review --summary "Added user authentication with JWT tokens"intrusive-thoughts serve
# Or from source:
bun run devOpen http://localhost:3456 to manage rules, configuration, and review history.
intrusive-thoughts runs in three modes: review, serve, and mcp.
Run a code review against the current branch's diff:
# Review current changes against main
intrusive-thoughts review --summary "Added user authentication with JWT tokens"
# Review against a specific branch
intrusive-thoughts review --summary "Refactored API layer" --base develop
# Review a specific repo directory
intrusive-thoughts review --summary "Fixed pagination bug" --dir /path/to/repo
# Use specific reviewer profiles
intrusive-thoughts review --summary "Auth changes" --reviewers security,general
# CI mode: in-memory DB, bundled defaults only, no disk state
intrusive-thoughts review --summary "PR review" --ephemeralOptions:
| Flag | Description | Default |
|---|---|---|
--summary |
Task description for the review (required) | -- |
--base |
Branch to diff against | From config (main) |
--dir |
Path to the git repository | Current directory |
--reviewers |
Comma-separated profile slugs | Auto-matched by file patterns |
--ephemeral |
In-memory DB, bundled defaults only (ideal for CI) | false |
The review result is printed to stdout as JSON:
{
"verdict": "approve",
"summary": "The changes look good overall...",
"comments": [
{
"file": "src/auth.ts",
"line": 42,
"severity": "warning",
"comment": "Consider adding rate limiting to login attempts."
}
],
"suggestions": [
"Add unit tests for the new authentication middleware."
],
"confidence": 0.88
}# Start with default port (3456)
intrusive-thoughts serve
# Start on a custom port
intrusive-thoughts serve --port 8080This serves both the REST API at /api/* and the web UI at the root.
intrusive-thoughts mcpThis starts an MCP server over stdio that exposes the review_code tool. See the MCP Configuration section below for how to connect it to your editor.
intrusive-thoughts exposes a single MCP tool called review_code that AI coding agents can call to get a structured code review.
| Parameter | Type | Required | Description |
|---|---|---|---|
taskSummary |
string | yes | Summary of the task/changes, compiled from user messages to the agent |
baseBranch |
string | no | Branch to diff against (defaults to configured base branch, usually main) |
workingDirectory |
string | no | Path to the git repository (defaults to the current working directory) |
Returns a JSON ReviewResult with verdict, summary, file-level comments, suggestions, and a confidence score.
Add to your Claude Code MCP configuration (typically ~/.claude/claude_desktop_config.json or your project's .mcp.json):
{
"mcpServers": {
"intrusive-thoughts": {
"command": "bun",
"args": ["run", "/absolute/path/to/intrusive-thoughts/src/index.ts", "mcp"],
"env": {
"ANTHROPIC_API_KEY": "sk-ant-..."
}
}
}
}Add to your OpenCode configuration file (opencode.json in your project root):
Add to your Cursor MCP settings (.cursor/mcp.json in your project root):
{
"mcpServers": {
"intrusive-thoughts": {
"command": "bun",
"args": ["run", "/absolute/path/to/intrusive-thoughts/src/index.ts", "mcp"],
"env": {
"ANTHROPIC_API_KEY": "sk-ant-..."
}
}
}
}- Replace
/absolute/path/to/intrusive-thoughtswith the actual path where you cloned the repo. - Set the appropriate API key in the
envblock. UseANTHROPIC_API_KEYfor the default Anthropic provider orOPENAI_API_KEYif you've configured OpenAI as your provider. - The MCP server inherits its provider/model configuration from the SQLite database. Use the web UI or REST API to change the provider before connecting via MCP.
intrusive-thoughts is designed to work in CI pipelines with minimal setup.
# Install
bun add -g intrusive-thoughts
# Run a review (ephemeral mode: no disk state, no HOME required)
ANTHROPIC_API_KEY=$SECRET intrusive-thoughts review \
--summary "PR #${PR_NUMBER}: ${PR_TITLE}" \
--ephemeralThe --ephemeral flag (or INTRUSIVE_THOUGHTS_EPHEMERAL=1 env var) is designed for CI:
- Uses an in-memory database — no disk writes, no state left behind
- Skips user-level and project-local lookups — uses only bundled defaults
- No HOME directory required — works in minimal containers
- Only needs an API key env var and a git repo
If you want CI to use your team's custom rules (without --ephemeral), commit a .intrusive-thoughts/ directory to your repo:
my-project/
.intrusive-thoughts/
rules/
no-todos.md
require-tests.md
reviewers/
ci-reviewer.md
Then run without --ephemeral:
intrusive-thoughts review --summary "PR review"The tool will discover and merge your project-local rules with the bundled defaults. The database will be created at the XDG data directory or a local fallback.
- name: AI Code Review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
bun add -g intrusive-thoughts
intrusive-thoughts review \
--summary "PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}" \
--base ${{ github.event.pull_request.base.ref }} \
--ephemeralintrusive-thoughts resolves configuration, rules, reviewer profiles, and the prompt template using a layered cascade. Higher-priority layers override lower ones:
| Priority | Layer | Location | Purpose |
|---|---|---|---|
| 1 (highest) | Environment variables | INTRUSIVE_THOUGHTS_* |
CI overrides, ephemeral runs |
| 2 | Project-local | .intrusive-thoughts/ in repo root |
Team-shared rules committed to the repo |
| 3 | User-level (XDG) | ~/.config/intrusive-thoughts/ |
Personal rules/profiles across all projects |
| 4 (lowest) | Bundled defaults | Shipped with the package | Always available, zero-config baseline |
Rules and reviewer profiles are merged across layers — a project-local rule with the same slug as a bundled rule overrides it, but other bundled rules remain active. The prompt template (code-review.md) uses first-found semantics: the highest-priority layer that has one wins.
Place a .intrusive-thoughts/ directory at your repository root to add project-specific configuration:
my-project/
.intrusive-thoughts/
rules/ # Project-specific review rules (.md files)
reviewers/ # Project-specific reviewer profiles (.md files)
code-review.md # Project-specific prompt template override
This is the recommended approach for teams — commit your .intrusive-thoughts/ directory to the repo so everyone (and CI) shares the same review rules. No external configuration needed.
Personal configuration follows the XDG Base Directory Specification:
| Type | Default location | Override env var |
|---|---|---|
| Config (rules, profiles, prompt) | ~/.config/intrusive-thoughts/ |
INTRUSIVE_THOUGHTS_CONFIG_DIR |
| Data (SQLite database) | ~/.local/share/intrusive-thoughts/data.db |
INTRUSIVE_THOUGHTS_DATA_DIR |
These directories respect XDG_CONFIG_HOME and XDG_DATA_HOME if set. If HOME is unavailable (e.g., in CI containers), the user-level layer is skipped entirely.
Backward compatibility: If the legacy ~/.intrusive-thoughts/data.db exists and no XDG-path database is found, the legacy location is used automatically.
Runtime configuration is stored in the SQLite config table:
| Key | Default | Description |
|---|---|---|
provider |
anthropic |
LLM provider (anthropic or openai) |
model |
claude-sonnet-4-20250514 |
Model identifier |
baseBranch |
main |
Default branch to diff against |
maxDiffLines |
5000 |
Line threshold before chunking kicks in |
chunkSize |
10 |
Maximum files per chunk |
httpPort |
3456 |
Default HTTP server port |
maxReviewRounds |
5 |
Max review rounds per MCP session |
fallbackProfile |
general |
Default reviewer when no profiles match |
API keys are always read from environment variables at runtime — they are never stored in the database.
| Provider | Environment Variable |
|---|---|
| Anthropic | ANTHROPIC_API_KEY |
| OpenAI | OPENAI_API_KEY |
| Variable | Description |
|---|---|
ANTHROPIC_API_KEY |
API key for the Anthropic provider |
OPENAI_API_KEY |
API key for the OpenAI provider |
INTRUSIVE_THOUGHTS_DB_PATH |
Override database file path (use :memory: for no persistence) |
INTRUSIVE_THOUGHTS_CONFIG_DIR |
Override user config directory |
INTRUSIVE_THOUGHTS_DATA_DIR |
Override user data directory |
INTRUSIVE_THOUGHTS_EPHEMERAL |
Set to 1 for ephemeral mode (same as --ephemeral flag) |
Via the web UI: Start the server with intrusive-thoughts serve and navigate to the Configuration page.
Via the REST API:
curl -X PUT http://localhost:3456/api/config \
-H "Content-Type: application/json" \
-d '{"provider": "openai", "model": "gpt-4o"}'Review rules are guidelines injected into every review prompt. They tell the LLM what to look for during code review. Each rule has a name, description, category, and severity.
style, security, performance, architecture, maintainability, general
critical, warning, suggestion
The following rules are seeded on first run:
| Rule | Category | Severity |
|---|---|---|
| No code duplication | maintainability | warning |
| No hardcoded colors | style | warning |
| No magic numbers | maintainability | suggestion |
| Error handling required | security | critical |
| No console.log in production code | style | suggestion |
Via the web UI: Start the server and navigate to the Rules page. You can add, edit, toggle, and delete rules.
Via the REST API:
# List all rules
curl http://localhost:3456/api/rules
# Create a rule
curl -X POST http://localhost:3456/api/rules \
-H "Content-Type: application/json" \
-d '{"name": "No TODO comments", "description": "TODOs should be tracked in issues, not left in code.", "category": "maintainability", "severity": "warning"}'
# Toggle a rule on/off
curl -X PATCH http://localhost:3456/api/rules/1/toggle
# Delete a rule
curl -X DELETE http://localhost:3456/api/rules/1The review prompt template uses {{variable}} placeholders that are interpolated at runtime with review context. It is resolved through the configuration cascade:
.intrusive-thoughts/code-review.mdin your repo (project-local override)~/.config/intrusive-thoughts/code-review.md(user-level override)prompts/code-review.mdin the package (bundled default)
When you edit the prompt through the web UI, the changes are saved to your user-level config directory (~/.config/intrusive-thoughts/code-review.md), not the bundled file.
| Variable | Description |
|---|---|
{{task_summary}} |
The task description provided by the calling agent |
{{rules}} |
Formatted block of all enabled review rules with severity |
{{diff}} |
The git diff content (or chunk of diff if chunked) |
{{changed_files}} |
List of changed files with status and line counts |
{{stats}} |
Overall diff statistics (additions, deletions, files changed) |
{{is_chunk}} |
"true" or "false" -- whether this is a partial chunk review |
{{chunk_info}} |
Human-readable chunk label, e.g. "Reviewing chunk 2 of 4" (empty if not chunked) |
Changes take effect on the next review -- no restart needed.
All endpoints are prefixed with /api.
| Method | Path | Description |
|---|---|---|
GET |
/api/rules |
List all rules |
POST |
/api/rules |
Create a rule |
PUT |
/api/rules/:id |
Update a rule |
DELETE |
/api/rules/:id |
Delete a rule |
PATCH |
/api/rules/:id/toggle |
Toggle enabled/disabled |
| Method | Path | Description |
|---|---|---|
GET |
/api/config |
Get all config key-value pairs |
PUT |
/api/config |
Update config values (partial update supported) |
| Method | Path | Description |
|---|---|---|
GET |
/api/prompt |
Get the current prompt template content |
PUT |
/api/prompt |
Update the prompt template |
| Method | Path | Description |
|---|---|---|
GET |
/api/reviews |
List review history |
GET |
/api/reviews/:id |
Get full review result by ID |
POST |
/api/reviews/run |
Trigger a review via HTTP |
The web UI is a React + Tailwind CSS single-page app served by the HTTP server.
| Page | Path | Description |
|---|---|---|
| Rules | /rules |
List, create, edit, toggle, and delete review rules |
| Configuration | /config |
Change provider, model, base branch, and other settings |
| Prompt Editor | /prompt |
Edit the review prompt template with a variable reference panel |
| Review History | /reviews |
Browse past reviews with verdict badges and timestamps |
| Review Detail | /reviews/:id |
Full result view with comments grouped by file, suggestions, and confidence score |
For setup, hot-reload workflow, and component conventions, see the Development Guide below.
This section covers everything a new contributor needs to get productive on the project.
- Bun v1.0+ (runtime, package manager, test runner, and bundler -- all in one)
- Git
- An API key for Anthropic or OpenAI (only needed to actually run reviews)
git clone https://github.com/your-org/intrusive-thoughts.git
cd intrusive-thoughts
bun installThe project has a backend (Bun + Express) and a frontend (React + Vite), and they run as two separate processes during development:
| Process | Command | Port | What it does |
|---|---|---|---|
| Backend | bun run dev |
3456 | Express API server, serves built static files from web/dist/ |
| Frontend | bun run dev:web |
5173 | Vite dev server with hot-reload, proxies /api to :3456 |
Important gotcha: If you edit web UI files and view http://localhost:3456, you will not see your changes -- that port serves the last production build. During frontend development, always use http://localhost:5173.
# Terminal 1: start the backend API server
bun run dev
# Terminal 2: start the Vite dev server with hot-reload
bun run dev:webThen open http://localhost:5173. The Vite dev server proxies all /api requests to the backend at :3456, so both the UI and API work seamlessly together.
These rules are non-negotiable across the entire backend codebase (src/):
| Rule | Details |
|---|---|
| Function length | 25 lines or fewer (body only, blank lines count) |
| Pure by default | Functions should be pure. Impure functions must have a @sideeffect JSDoc annotation |
No any |
Use unknown and narrow with type guards or Zod |
| No default exports | Named exports only, everywhere |
| No mutable module-level state | No top-level let or module singletons |
| Dependency injection | DB handles, file paths, and provider instances are passed as parameters, never imported as singletons |
| Explicit return types | All exported functions must declare their return type |
TypeScript is configured with strict: true, noUnusedLocals, noUnusedParameters, and noFallthroughCasesInSwitch in both tsconfig.json (backend) and web/tsconfig.json (frontend).
The frontend lives in web/src/ and uses React 19, Tailwind CSS v4, and Radix UI primitives.
Design system:
- Light paper-inspired theme:
stone-50background,slate-800dark sidebar - Accent color is warm charcoal (
stone-800/stone-900) for buttons and interactive elements - No purples anywhere -- the palette is stone, slate, and semantic colors (red, amber, emerald, sky, teal)
Shared UI components live in web/src/components/ui/:
| Component | File | Notes |
|---|---|---|
| Button | ui/Button.tsx |
Variants: primary, secondary, ghost, danger. Sizes: sm, md |
| Badge | ui/Badge.tsx |
Semantic variants for verdicts, severities, and categories. Also exports VerdictBadge, SeverityBadge, CategoryBadge |
| Card | ui/Card.tsx |
Card, CardHeader, CardBody compound components |
| Select | ui/Select.tsx |
Wraps @radix-ui/react-select |
| Switch | ui/Switch.tsx |
Wraps @radix-ui/react-switch |
| Dialog | ui/Dialog.tsx |
Wraps @radix-ui/react-dialog |
| Tooltip | ui/Tooltip.tsx |
Wraps @radix-ui/react-tooltip |
Styling pattern: All components use the cn() utility from web/src/lib/utils.ts (clsx + tailwind-merge) for class merging. Variant styles are defined as Record<Variant, string> constants, not computed dynamically -- this avoids the Tailwind purge issue where dynamic class names get stripped from the build.
API calls: Use the useApi hook from web/src/hooks/useApi.ts for typed fetch calls to the backend.
Tests use Bun's built-in test runner (bun:test). No external test framework is needed.
# Run all 140 tests
bun test
# Run a specific test file
bun test tests/core/context/chunker.test.ts
# Watch mode
bun test --watch
# Coverage
bun test --coverageTest database pattern: Tests that need a database use createTestDb() from tests/db/helpers.ts, which creates a fresh in-memory SQLite instance (:memory:) with the schema and migrations applied. No cleanup needed -- the database disappears when the test ends.
Test fixtures live in tests/fixtures/ and include sample diffs, LLM response payloads, and prompt templates.
The test suite includes 140 tests across 17 test files covering the database layer, git diff parsing, chunking logic, rules engine, prompt interpolation, LLM providers (mocked), the review orchestrator, the core review pipeline, and all REST API endpoints.
The backend and frontend have separate TypeScript configs. Check both:
# Backend (src/)
bunx tsc --noEmit
# Frontend (web/src/)
bunx tsc --noEmit -p web/tsconfig.jsonBoth must pass with zero errors before submitting changes.
# Build the web UI (outputs to web/dist/)
bun run build:web
# Build the backend (outputs to dist/)
bun run buildAfter building, bun run dev (or bun run src/index.ts serve) will serve the built frontend from web/dist/ alongside the API.
- SQLite via
bun:sqlite(built into Bun, no native addon) - Default location:
~/.local/share/intrusive-thoughts/data.db(XDG-compliant) - Legacy location
~/.intrusive-thoughts/data.dbis auto-detected for backward compatibility - Override with
INTRUSIVE_THOUGHTS_DB_PATHenvironment variable - Use
:memory:for tests (seeopenDatabase(":memory:")orcreateTestDb()) or CI (--ephemeral) - Schema and migrations run automatically on open -- no manual migration step
- WAL mode is enabled for concurrent read performance
Agent (or CLI user)
|
+-- provides: taskSummary, [baseBranch], [workingDir]
|
v
MCP Tool / CLI / REST API
|
+-- 1. Resolve paths (env > project-local > user XDG > bundled defaults)
+-- 2. Open SQLite database, load config
+-- 3. Seed rules + profiles from all resolved directories (merged by slug)
+-- 4. Run git diff <base>...HEAD
+-- 5. Match reviewer profiles to changed files
+-- 6. Build ReviewContext (diff + summary + files + stats + rules)
+-- 7. If diff > maxDiffLines -> smart chunking (split by directory)
+-- 8. Load prompt template (first found in cascade)
+-- 9. Interpolate context into prompt
+-- 10. Send to configured LLM (Anthropic or OpenAI)
| (if chunked: one call per chunk, then a synthesis call)
+-- 11. Parse structured ReviewResult from LLM response
+-- 12. Save review to SQLite history
|
v
Return ReviewResult JSON
When a diff exceeds the maxDiffLines threshold (default: 5000 lines), intrusive-thoughts splits it into smaller chunks:
- Files are grouped by parent directory
- Each chunk contains at most
chunkSizefiles (default: 10) - Each chunk is reviewed independently with full task context and rules
- A final synthesis LLM call combines all chunk results into a single unified verdict
intrusive-thoughts/
+-- src/
| +-- index.ts # Entrypoint: mode detection and dispatch
| +-- cli.ts # CLI argument parsing
| +-- paths.ts # Layered path resolution (env > project > user > bundled)
| +-- types.ts # All shared TypeScript types and error classes
| +-- db/ # SQLite database layer (bun:sqlite)
| +-- core/
| | +-- review.ts # Main review entry point (all interfaces call this)
| | +-- context/ # Git diff, chunking, context assembly
| | +-- reviewer/ # Prompt loading, LLM providers, orchestrator
| | +-- rules/ # Rules engine and default seeds
| +-- server/
| | +-- mcp.ts # MCP server (stdio transport)
| | +-- http.ts # Express HTTP server (REST API + static files)
| +-- api/ # REST API route handlers
+-- web/ # React + Vite + Tailwind frontend
+-- prompts/
| +-- code-review.md # Bundled review prompt template (default)
| +-- rules/ # Bundled review rules (.md files)
| +-- reviewers/ # Bundled reviewer profiles (.md files)
+-- tests/ # Unit and integration tests
+-- bin/
+-- intrusive-thoughts.ts # CLI shim
~/.config/intrusive-thoughts/ # User-level overrides (XDG_CONFIG_HOME)
+-- rules/ # Personal rules (applied to all projects)
+-- reviewers/ # Personal reviewer profiles
+-- code-review.md # Personal prompt template override
~/.local/share/intrusive-thoughts/ # User-level data (XDG_DATA_HOME)
+-- data.db # SQLite database
my-project/.intrusive-thoughts/ # Project-local overrides (committed to repo)
+-- rules/ # Project-specific rules
+-- reviewers/ # Project-specific reviewer profiles
+-- code-review.md # Project-specific prompt override
MIT
{ "$schema": "https://opencode.ai/config.json", "mcp": { "intrusive-thoughts": { "type": "local", "command": ["bun", "run", "/absolute/path/to/intrusive-thoughts/src/index.ts", "mcp"], "enabled": true, "environment": { "ANTHROPIC_API_KEY": "sk-ant-..." } } } }