Skip to content
harshithKumar12Public

About

every failed agent trace is automatically turned into a regression test, so your agent never makes the same mistake twice

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Grudge logo

Grudge

Deterministic regression scars for AI agents

every failed agent trace is automatically turned into a regression test, so your agent never makes the same mistake twice.

npm version PyPI version MIT License Node.js 18 or newer Python 3.11 to 3.14

Quick start · How it works · CLI reference · Development

The 30-second version

Your agent does something it shouldn't: sends a file it read to an external endpoint, runs a destructive shell command, leaks a key into a log. You fix the prompt. Two weeks later a different model or a different tool result brings the exact same mistake back — because nothing in your test suite ever described it.

Grudge turns that failure into a scar: a shrunk replay cassette plus a deterministic predicate that runs in CI and blocks the call locally before it happens.

grudge setup -y                          # install the Claude Code hook here
grudge propose \
  --rule 'never call shell where command ~ "rm -rf"' \
  --why "agent deleted the venv" --accept
grudge verify                            # Result: PASS (10/10 checks)

That is the whole loop — a rule a hook actually enforces, proven by feeding real PreToolUse payloads through the installed guard rather than trusting it.

  • No model in the loop. Replay is offline, deterministic, and needs no API key.
  • Fails red then green. A scar is only admitted with bad-agent, fixed-agent, and benign-control evidence, so it cannot pass for the wrong reason.
  • Runs before the call. The Claude Code hook exits 2 and explains why; it does not review the action afterwards.

npm install -g grudge-cli · pip install grudge-cli · Verify it works

Table of contents

Overview

AI agents can repeat the same unsafe or incorrect action after a prompt change, tool-result change, or model change. A transcript can show the failure, but it does not provide a compact regression test that can be replayed against the live policy.

Grudge turns the failure into a Scar:

  1. Capture the agent's typed trace.
  2. Freeze only the external world in a replay cassette.
  3. Shrink the cassette to the smallest causal example.
  4. Describe the failure with a readable deterministic predicate.
  5. Prove that the bad behavior is red and the fixed behavior is green.
  6. Run the Scar again in CI or enforce it locally before a tool executes.

The core workflow is local-first, deterministic, and designed to work without provider credentials or network access.

Why Grudge

Problem Grudge's answer
Long transcripts are difficult to review Store a compact cassette and predicate
Replaying a transcript can hide policy changes Replay the world while keeping the policy live
Generated rules can be vague Compile and evaluate deterministic DSL predicates
A one-off test is easy to over-trust Require red/green admission evidence and benign controls
Agents can repeat a known mistake locally Install a lightweight Node Guard with Claude Code hooks
Tool output may contain secrets Redact captured content before persistence

Features

Python engine

  • Typed trace capture and schema validation.
  • Secret-aware redaction for recorded traces.
  • Strict cassette replay with ordered, single-use entries.
  • Delta debugging and content reducers for shrinking failures.
  • Deterministic predicates for call shape, ordering, and MVP taint flow.
  • Scar admission with bad-agent, fixed-agent, and benign-control evidence.
  • Wilson-bound statistical gating for repeated checks.
  • Offline demo, JSON output, Markdown/Rich reports, and a local event viewer.

Node Guard

  • Interactive setup for repository, user, or all-project scopes.
  • Claude Code hook registration for SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, and Stop.
  • Deterministic blocking of admitted mistakes before a tool runs.
  • verify proves the installed guard really blocks a call, offline, in one command.
  • Duplicate hook accounting protection across overlapping scopes.
  • Status-line reporting with estimated savings.
  • doctor diagnostics, dry-run setup, timestamped backups, and safe uninstall.
  • Selective cleanup that preserves user content and third-party hooks/status lines.
  • Self-contained bundled runtime with no npm runtime dependencies.

Documentation and operations

  • Architecture, trust-model, integration, hook, and token-budget docs.
  • Local package smoke tests for npm and Python artifacts.
  • Release metadata consistency checks.
  • No telemetry, hosted service, MCP server, or runtime network requirement.

How it works

agent run
    │
    ▼
typed trace ──► cassette ──► shrinker ──► minimal causal world
    │                                      │
    │                                      ▼
    └──────────────────────────────► predicate and Scar
                                           │
                                           ▼
                              admission proof and CI gate
                                           │
                                           ▼
                              local Guard enforcement

The three Scar artifacts

  1. Cassette — the frozen external world. Replay serves only matching, single-use responses; the policy still computes its next action.
  2. Predicate — the readable never-again rule. It describes the call shape, ordering, or taint flow that identifies the failure.
  3. Verdict — the evidence that the rule discriminates correctly: red on the vulnerable path, green on the fixed path, and nonempty benign controls.

Architecture

Grudge pipeline architecture

The engine is CLI-first. The recorder emits typed events, replay freezes only the external world, the shrinker minimizes the cassette, deterministic code evaluates predicates, admission proves the Scar, and the gate turns repeated clean runs into a decision.

The local viewer consumes demo events over a local SSE server. It does not own pipeline state or verdict logic.

For the detailed contracts, see:

Quick start

1. Install the local Node Guard

From any repository where you want local protection:

npx grudge-cli@latest setup
npx grudge-cli@latest doctor

npx runs the package temporarily; it does not create a permanent grudge command in PowerShell. To use the Node Guard directly as grudge, install the package globally:

npm install --global grudge-cli
Get-Command grudge
grudge --version
grudge doctor

For this checkout, install the local build instead of the registry package:

npm --prefix packages/grudge run build
npm install --global .\packages\grudge
grudge --version

If PowerShell still says that grudge is not recognized, add npm's global binary directory to your user PATH, open a new PowerShell window, and retry:

$npmBin = npm prefix --global
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if (($userPath -split ';') -notcontains $npmBin) {
  [Environment]::SetEnvironmentVariable("Path", "$userPath;$npmBin", "User")
}
$env:Path = "$npmBin;$env:Path"
grudge --version

If both packages are installed, use the explicit aliases to avoid ambiguity:

grudge-node doctor       # npm Node Guard
grudge-engine --help     # Python engine

In a project directory on a machine where Claude Code is installed, the whole install is -y:

grudge setup -y

Both --agents and --scope are already defaulted: the scope is repo, and the agents are whatever setup detects. The flagless form writes the same files as grudge setup --yes --agents claude-code --scope repo — you only need the flags to deviate from what was detected, for example grudge setup -y --scope global for an all-projects install.

Every command also has a short form, so grudge-engine run -s scars -a examples.leaky_agent.agent_fixed -j works instead of grudge-engine run --scars scars --agent examples.leaky_agent.agent_fixed --json. Long forms always work too; grudge --help and grudge-engine --help list both.

Choose which agents to configure

setup detects the coding agents on this machine and opens a picker. Space toggles an entry, Enter confirms; the first entry is preselected because Claude Code is the only Guard-tier integration:

◆ Which agents should use Grudge?
❯ ◉ Claude Code Guard — detected
  ◯ Codex Advisory — detected
  ◯ Cursor Advisory — not detected
  ◯ Gemini CLI Advisory — detected
  ◯ GitHub Copilot CLI Advisory — detected
  ◯ Windsurf Advisory — not detected
  ◯ OpenCode Advisory — detected

To skip the picker and accept the defaults — Claude Code, this repository — one flag is enough:

npx grudge-cli@latest setup -y

Pass --agents and --scope only to deviate from what was detected. --agents takes a comma-separated list of claude-code, opencode, codex, gemini-cli, copilot, windsurf, and cursor:

# Several agents at once; Codex and OpenCode share one AGENTS.md block
npx grudge-cli@latest setup --yes --agents claude-code,opencode,codex --scope repo

# OpenCode only (writes AGENTS.md)
npx grudge-cli@latest setup --yes --agents opencode --scope repo

# Claude Code for every project on this machine
npx grudge-cli@latest setup --yes --scope global

Preview the exact file list without writing anything:

npx grudge-cli@latest setup --dry-run --json --yes --agents claude-code,opencode --scope repo

Remove only Grudge-managed content:

npx grudge-cli@latest uninstall

See Integrations for what each agent gets and What Grudge writes into your files for the exact block that lands in AGENTS.md.

2. Install the Python engine

python -m pip install grudge-cli
grudge-engine --help

The Python package also provides grudge, but the npm Node Guard and Python engine intentionally share that short name for compatibility. When both are installed, prefer grudge-node and grudge-engine.

For a checkout-based development install:

python -m pip install -e .

3. Run the offline demo

grudge-engine demo --no-browser --fast --json

To run the two-session Claude-style hook proof:

grudge-engine demo --guard --json

The demo uses checked-in fake tools and scripted policies. It does not call a model provider or require public-network access.

demo --guard ships its own script and payloads inside the wheel, so it works from an installed package with no checkout. It still needs a Node Guard runtime, resolved in this order: --node-runtime PATH, then GRUDGE_NODE_RUNTIME, then a repository checkout, then packages/grudge/node_modules. Outside a checkout, set one of the first two to the installed dist/grudge.mjs (for example node_modules/grudge-cli/dist/grudge.mjs from the npm package).

Verify it works

One command proves the installed Guard blocks a real tool call. It needs no model, no network, and no Python:

npx grudge-cli@latest verify

It creates a temporary project, installs the guard there, proposes and accepts a harmless rule, then feeds the installed hook runner real Claude Code PreToolUse payloads over stdin. Expected last two lines:

Result: PASS (10/10 checks)
Temp project removed (use --keep to inspect it)

The checklist covers the install, the installed runtime, event capture, propose, accept, the block (Bash "echo GRUDGE_TEST" exits 2 with a reason), a benign call (echo hello exits 0), fail-open on malformed input, the stats log, and grudge report. A failure keeps the temp project and prints its path; --keep keeps it after a pass too. grudge verify --json is machine-readable.

Scope of the claim: Guard enforcement is verified for Claude Code only, because only Claude Code exposes blocking command hooks. Codex, Gemini CLI, GitHub Copilot, OpenCode, and Windsurf receive an instruction file and are advisory: nothing stops their tool calls. grudge doctor checks files and configuration; it never claims that blocking works. Only grudge verify (or a live Claude Code run) does that.

Install the packages

Grudge has two complementary packages with the same user-facing command name:

Package Install command Main responsibility
grudge-cli on PyPI python -m pip install grudge-cli Full trace, replay, shrinking, admission, gates, and demo
grudge-cli on npm npx grudge-cli setup Local Node Guard, hooks, setup, doctor, and uninstall

Exercise the npm package from this checkout

From the repository root:

npm --prefix packages/grudge run build
npm --prefix packages/grudge run setup:repo
npm --prefix packages/grudge run doctor:repo

From packages/grudge itself:

npm run build
npm run setup:repo
npm run doctor:repo
npm run uninstall:repo

Do not combine cd packages/grudge with the root-relative path packages/grudge/dist/grudge.mjs; use node dist/grudge.mjs from that directory or node packages/grudge/dist/grudge.mjs from the repository root.

Docker

The project is local-first and does not require Docker for normal use. Docker is useful for a clean Linux smoke environment or CI job.

Example development container:

FROM python:3.14-slim

WORKDIR /workspace
COPY . .

RUN python -m pip install --upgrade pip \
    && python -m pip install -e .

CMD ["python", "-m", "grudge", "demo", "--no-browser", "--fast", "--json"]

Build and run it from the repository root:

docker run --rm grudge-demo

The Docker path is intended for headless Python validation. The Node Guard requires access to the agent configuration files it is installing into, so use the host environment when testing Claude Code hook behavior.

CLI reference

Run grudge-engine --help or append --help to any Python command. Implemented Python commands include:

grudge-engine capture AGENT_MODULE --out PATH
grudge-engine shrink TRACE --failure RULE --out PATH
grudge-engine admit CASSETTE --bad AGENT_MODULE --good AGENT_MODULE --out PATH
grudge-engine run --scars PATH --agent AGENT_MODULE
grudge-engine demo [--guard] [--no-browser] [--fast] [--json]
grudge-engine scars list [--scars PATH]
grudge-engine scars show SCAR [--scars PATH]

The Node Guard runtime provides:

grudge setup [--yes] [--dry-run] [--scope repo|global|all] [--agents LIST]
grudge verify [--keep] [--json]
grudge doctor
grudge uninstall [--json]
grudge hook SessionStart|UserPromptSubmit|PreToolUse|PostToolUse|Stop
grudge propose --rule RULE --why EXPLANATION [--preventive] [--accept]
grudge propose --rule-stdin --why EXPLANATION [--preventive] [--accept]
grudge accept SCAR_ID
grudge scars list|show
grudge stats
grudge report
grudge statusline

propose records a pending scar by default and prints the exact command that enforces it. --accept skips that review step and enforces the rule immediately, which is convenient and riskier: a bad rule blocks work until you remove it with grudge scars remove. Unknown options fail with exit code 2 and list the valid flags instead of being ignored.

A PreToolUse deny exits 2 and prints the deny JSON, so Claude Code blocks the call even if it cannot parse stdout.

Expected input and engine failures use exit code 2. A passing gate returns 0; a blocked gate returns 1.

By default, commands print structured human-readable ASCII output with grouped checks, status marks, a summary, and a suggested next command. Use --json for stable machine-readable output and --debug when a stack trace is needed; --ascii and NO_COLOR=1 are accepted and change nothing because output never emits ANSI color. Claude Code hook stdout remains machine-readable JSON because the host consumes it directly. See docs/cli-output.md for the complete command matrix.

Integrations

Grudge installs one of two things per agent:

  • Guard — real hooks. PreToolUse can deny a tool call before it runs.
  • Advisory — a managed instruction block. The model is told to record and avoid mistakes, but nothing blocks a call, so compliance depends on the model reading the block.

Everything else — capture, shrink, admit, gates, scars, stats — is the same CLI no matter which agent you use.

Where each agent gets configured

Integration Tier Repository scope (--scope repo / local) User scope (--scope global)
Claude Code Guard .claude/settings.json or .claude/settings.local.json, plus CLAUDE.md ~/.claude/settings.json plus ~/.claude/CLAUDE.md
OpenCode Advisory AGENTS.md ~/.config/opencode/AGENTS.md
Codex Advisory AGENTS.md ~/.codex/AGENTS.md
Gemini CLI Advisory GEMINI.md ~/.gemini/GEMINI.md
GitHub Copilot CLI Advisory .github/copilot-instructions.md none; paste the block yourself
Windsurf Advisory .windsurfrules ~/.codeium/windsurf/memories/global_rules.md
Cursor Unverified none none

--scope local is the personal variant of a repository install: Claude Code's settings go to .claude/settings.local.json while the instruction block still goes to the shared CLAUDE.md.

Codex and OpenCode both read a repository AGENTS.md, so enabling both writes that file once and keeps a single managed block.

Claude Code (Guard)

npx grudge-cli@latest setup -y

Setup registers five hooks and, only when no other tool owns it, a status line:

Hook What it does
SessionStart Injects up to 8 relevant Scar one-liners (capped at 1,200 chars)
UserPromptSubmit Adds a 200-char nudge to record a mistake, only on correction phrases
PreToolUse Denies a call that matches an admitted Scar
PostToolUse Appends the redacted event to .grudge/session/
Stop Prints one line only when this turn caught something

The hook command is always plain Node with absolute paths, never npx and never Python:

"<node>" "<repo>/.grudge/bin/grudge.mjs" hook PreToolUse      # repository scope
"<node>" "<home>/.grudge/bin/grudge.mjs" hook PreToolUse      # user scope

Absolute paths matter: Claude Code runs the hook from the session's current directory, so a relative .grudge/bin/grudge.mjs stops resolving after a cd and the host treats the failure as "allowed". grudge doctor fails its hook paths check if a registered grudge.mjs command is relative or missing.

Absolute paths are necessary but not sufficient. Claude Code loads a project's .claude/settings.json from the directory the session starts in; it does not walk up to the repository root. A session started in docs/ therefore runs with no Grudge hooks at all, however the command is registered. Start Claude Code at the repository root, or point it at the file explicitly:

cd <repo> && claude
claude --settings <repo>/.claude/settings.json   # session started elsewhere

--scope local is the exception: Claude Code reads .claude/settings.local.json from the git repository root, so on Linux and macOS it still loads from a subdirectory of the same repository (on Windows it does not). A user-scope install (--scope global) writes ~/.claude/settings.json, which Claude Code loads in every project. Grudge never changes user-level settings on its own; that remains an explicit choice. See docs/hooks.md for the measured behaviour.

A user-scope install keeps its rules in ~/.grudge/compiled.json and those rules apply inside every project. Precedence is deterministic: global rules are loaded first and are authoritative, project rules add protections, and a project rule with the same id as a global rule is ignored with a warning so a repository can never weaken the machine-wide set. mode: "off" applies to one installation only, so switching a project off does not disable global protection.

An existing third-party statusLine is preserved and reported, never overwritten. Grudge's own line is short and marked as an estimate:

grudge: 0 scars, 0 blocked, ~0 tokens saved (est.)

Claude Code reads CLAUDE.md, so that is where the instruction block goes. Use grudge doctor inside the project to confirm the hooks, the block, and the status line.

OpenCode (Advisory)

npx grudge-cli@latest setup --yes --agents opencode --scope repo    # AGENTS.md
npx grudge-cli@latest setup --yes --agents opencode --scope global  # ~/.config/opencode/AGENTS.md

OpenCode exposes no hook surface, so Grudge only adds the managed block to AGENTS.md. Nothing intercepts a tool call; the block tells the model to record a repeatable mistake once and to call grudge propose instead of retrying.

Codex (Advisory)

npx grudge-cli@latest setup --yes --agents codex --scope repo    # AGENTS.md
npx grudge-cli@latest setup --yes --agents codex --scope global  # ~/.codex/AGENTS.md

Codex shares AGENTS.md with OpenCode, so enabling both is a single block in one file.

Gemini CLI (Advisory)

npx grudge-cli@latest setup --yes --agents gemini-cli --scope repo    # GEMINI.md
npx grudge-cli@latest setup --yes --agents gemini-cli --scope global  # ~/.gemini/GEMINI.md

GitHub Copilot CLI (Advisory)

npx grudge-cli@latest setup --yes --agents copilot --scope repo

Copilot's documented repository instruction file is .github/copilot-instructions.md. Copilot has no user-level target, so --scope global writes no Copilot file at all; paste the block from What Grudge writes into your files into the instruction file your Copilot CLI build reads.

Windsurf (Advisory)

npx grudge-cli@latest setup --yes --agents windsurf --scope repo    # .windsurfrules
npx grudge-cli@latest setup --yes --agents windsurf --scope global  # ~/.codeium/windsurf/memories/global_rules.md

Cursor (Unverified)

The Cursor adapter writes nothing and prints the block instead. Cursor's current public rules format was not verified in this project, so Grudge will not guess at a filename:

npx grudge-cli@latest setup --yes --agents cursor --scope repo

Paste the block from What Grudge writes into your files into your Cursor rules file (.cursor/rules/*.mdc) yourself.

Several agents in one run

npx grudge-cli@latest setup --yes \
  --agents claude-code,opencode,codex,gemini-cli,copilot,windsurf \
  --scope repo

That run touches .claude/settings.json, CLAUDE.md, AGENTS.md (once), GEMINI.md, .github/copilot-instructions.md, .windsurfrules, .grudge/bin/grudge.mjs, .gitignore, and .grudge/config.json. Confirm the list before writing with --dry-run --json.

See the full integration matrix and hook contract.

What Grudge writes into your files

The managed instruction block

Every agent receives the same block, delimited by stable markers. This is the exact text, and it is the only thing Grudge adds to AGENTS.md:

<!-- grudge:begin v1 -->
## Grudge (mistake guard)
- A hook may block a tool call and cite a "scar". Do not retry the same call or work around it. Change approach, or ask the user if you think it is a false positive.
- When the user corrects a repeatable mistake, or something failed because of an action you should never repeat in this repo, record it once:
  `node .grudge/bin/grudge.mjs propose --rule '<rule>' --why '<one sentence>'`
- Rule forms: `never call <tool> where <field> ~ "<text>"`, `must call <tool> before <tool>`, `never call <tool> where <field> tainted_by <tool>`. Tools: read_file, write_file, shell, http. Run `... propose --help` for examples.
- One rule per correction. Never propose rules that block reading files or running tests. Do not edit `.grudge/`.
<!-- grudge:end -->

How that block behaves:

  • Only the bytes between <!-- grudge:begin v1 --> and <!-- grudge:end --> belong to Grudge. Your own headings, rules, and notes in AGENTS.md, CLAUDE.md, GEMINI.md, .windsurfrules, or copilot-instructions.md are left untouched.
  • Running setup again replaces the block in place, so setup is idempotent and safe to re-run after a Grudge upgrade.
  • grudge uninstall removes only that block, and deletes a file only when Grudge created it and nothing else remains in it. Inside .grudge it removes Grudge's own names (config, compiled rules, stats, session/scar/pending/bin directories) and leaves anything you put there, printing the preserved paths.
  • Codex and OpenCode read the same repository AGENTS.md, so enabling both produces one block, not two.
  • The block is deliberately small (well under the 12-line / 1,000-character budget) and grudge doctor measures it.

Do you have to edit AGENTS.md yourself?

Not when the agent is supported: setup writes the block into the right file for the scope you chose, and grudge doctor reports whether the project and user instruction blocks are present.

You do edit it yourself when:

  • the agent is not in the table — for example Cursor, where setup prints the block instead of guessing a filename;
  • you want the guard in an agent Grudge has never heard of: paste the block at the end of that agent's instruction file and keep the markers.

One caveat with a user-scope install: the block's command line is repository-relative (node .grudge/bin/grudge.mjs). That path only exists after a repository install. With --scope global there is no local runtime, so call the global command instead:

grudge propose --rule '<rule>' --why '<one sentence>'

The rest of the files

Path Written by Contents
.grudge/bin/grudge.mjs setup Bundled Node runtime used by the hooks and status line
.grudge/config.json setup packageVersion, scriptHash, selected adapters, mode, autoAccept, lastBackup
.grudge/compiled.json Python engine Flattened admitted Scars the hook evaluates; absent means nothing to enforce yet
.grudge/pending/, .grudge/scars/ grudge propose / grudge accept Proposals waiting for human approval, and accepted Scars
.grudge/session/ PostToolUse Redacted per-session event log (gitignored, 7-day retention)
.grudge/stats.json, .grudge/stats.jsonl hooks Counters and the append-only event log (gitignored)
.grudge/backups/<timestamp>/ setup Byte copies of every file it changed, plus manifest.json

setup also appends exactly three lines to .gitignore:

.grudge/session/
.grudge/stats.json
.grudge/*.log

Turning it off without uninstalling

GRUDGE_DISABLE=1 claude          # PowerShell: $env:GRUDGE_DISABLE = "1"

or set "mode": "off" in .grudge/config.json. Both leave the files in place; grudge uninstall is the removal path.

What happens when Grudge cannot decide

A call that matches a rule is always denied, before any bookkeeping runs. Grudge fails open only when the failure is about infrastructure or input, so nothing actually proves the call is prohibited:

Situation Behavior
Rule matches a prohibited call Blocked (exit 2) with the scar ID and reason
Rule loads slower than hookBudgetMs Allowed, with a stderr warning
.grudge/compiled.json missing or corrupt Allowed; a corrupt file cannot invent a match
Malformed or oversized hook input Allowed, with an explicit warning
Stats or session log write fails Decision unchanged; bookkeeping is bounded and dropped
Same hook event delivered twice Still blocked; only the counter is deduplicated

The time budget covers reading state, not evaluating rules, so a slow disk cannot turn a matching call into an allow. Use grudge verify when you need to prove blocking on the actual installation.

Project structure

grudge/
├── grudge/                 # Python engine
├── examples/               # Vulnerable and fixed demo agents
├── packages/grudge/        # Node Guard package and bundled runtime
├── tests/                  # Python tests
├── packages/grudge/test/   # Node tests
├── docs/                   # Architecture, contracts, and guides
├── scripts/                # Documentation and release checks
├── pyproject.toml          # Python package metadata
├── Makefile                # Reproducible development commands
└── CHANGELOG.md            # Release history

Configuration and data

Grudge stores local runtime state in .grudge/ when the Node Guard is enabled. This includes the compiled rules, pending proposals, scars, session events, stats, and timestamped setup backups. The file table lists each one.

The installer may update, depending on the agents you select:

  • .claude/settings.json, .claude/settings.local.json, or ~/.claude/settings.json for Claude Code hooks and the status line;
  • the scope-matching instruction file: CLAUDE.md, AGENTS.md, GEMINI.md, .windsurfrules, .github/copilot-instructions.md, ~/.codex/AGENTS.md, ~/.config/opencode/AGENTS.md, ~/.gemini/GEMINI.md, or ~/.codeium/windsurf/memories/global_rules.md;
  • .gitignore entries for local Grudge state;
  • .grudge/ itself, including the bundled runtime.

Setup never overwrites a whole file. It merges settings, replaces only the marked instruction block, and preserves unrelated user content plus third-party hooks and status lines. Inspect a plan first with --dry-run; use doctor after setup.

Testing

Run the primary checks from the repository root:

npm test --prefix packages/grudge
python -m pytest -q
ruff check .
mypy .
python -m scripts.docs_check
python -m scripts.release_check
git diff --check

Build package artifacts without publishing:

# `npm pack` and `npm publish` read package.json from the current directory;
# only commands like `npm test` honour --prefix.
(cd packages/grudge && npm pack)
python -m build --outdir release-dist
python -m twine check release-dist/*

The repository's focused checks are the recommended release gate. Whole-tree ruff check . and mypy . pass today, so a finding in either command is a real regression rather than expected noise.

The three-OS CI matrix (.github/workflows/grudge-guard.yml) runs the Node and Python suites, lint, types, docs/release checks, and npm pack --dry-run on Ubuntu, macOS, and Windows for Python 3.11 and 3.14. Two extra jobs cover the parts a single local machine cannot: the Node suite on Node 18/20/22, and a clean install of both built artifacts outside the checkout followed by grudge-engine demo --guard --json to prove blocking still works.

Those CI jobs were added on a Windows-only machine. They are not locally verified as runner configuration; the first dispatched run is their first real execution. The commands inside them are the same ones verified locally.

Troubleshooting

npm warns about allow-scripts

You may see:

npm warn Unknown env config "allow-scripts".
npm warn Unknown user config "allow-scripts".

These are npm configuration warnings from the local environment, not Grudge runtime failures. If the setting is obsolete in your environment, inspect it with npm config get allow-scripts and remove it with npm config delete allow-scripts.

Version bump tests still expect the previous version

When moving from 0.1.8 to 0.1.9, update all version surfaces together:

  • packages/grudge/package.json;
  • packages/grudge/package-lock.json;
  • packages/grudge/src/version.js;
  • pyproject.toml;
  • release headings and documentation;
  • version assertions in packages/grudge/test/;
  • release-check expectations.

The bundled-runtime test and CLI version test intentionally catch stale version references. A failure that still expects 0.1.6 means the version bump is incomplete; do not weaken the test.

PowerShell strips rule quotes

Pass the DSL through stdin:

'never call shell where command ~ "GRUDGE_TEST"' |
  grudge propose --rule-stdin --why "harmless test rule" --preventive

Setup changes the wrong scope

Use explicit scope flags:

npx grudge-cli@latest setup --yes --scope repo
npx grudge-cli@latest setup --yes --scope global

Then run grudge doctor and inspect the reported settings and instruction files.

Install once, guard every project

A user-scope install is enough for normal use. The hooks live in ~/.claude/settings.json, run ~/.grudge/bin/grudge.mjs, and read the project directory from the hook payload, so they act on whichever project Claude Code is open in. Do not run setup again inside each repository:

npx grudge-cli@latest setup --scope global --yes

Each project only needs its own .grudge/ state (compiled scars, session log, stats). That state appears the first time you run a Grudge command in the project, and nothing is written there before that. From inside a project with no local install, grudge doctor reports the user-scope install as install scope: user (...) instead of calling Grudge missing.

A third-party status line already exists

Grudge preserves an existing third-party status line and reports a warning. It does not silently replace another tool's status-line configuration.

If that preserved command points at a script that is not on disk, setup and grudge doctor also report which path is missing. Claude Code runs the command in the project directory, so a relative path such as ${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/tool-statusline.cjs fails in every project that does not contain that file and Claude Code then shows no status line at all. Grudge reports the problem and leaves the setting untouched; fix the command yourself, for example by pointing it at an absolute path:

{
  "statusLine": {
    "type": "command",
    "command": "node \"C:/Users/you/.claude/helpers/tool-statusline.cjs\""
  }
}

Development

Create a virtual environment and install the locked development dependencies:

make install
make test
make lint
make typecheck
make docs-check
make release-check

The Node package has no runtime npm dependencies. The Python package declares its runtime dependencies in pyproject.toml; development tools are available through the dev optional dependency.

Useful development commands

python -m grudge demo --no-browser --fast --json
python -m grudge demo --guard --json
npm --prefix packages/grudge run build
npm --prefix packages/grudge run doctor:repo

Release checklist

Release status: Grudge CLI 0.2.3 is published in both public package registries (grudge-cli on npm and on PyPI). The unrelated grudge package on npm is a different project, so install grudge-cli.

Before publishing a new version:

  1. Update the npm and Python version metadata.
  2. Update CHANGELOG.md, README release status, and package docs.
  3. Run Node tests, Python tests, lint, mypy, docs, and release checks.
  4. Build the npm tarball and Python sdist/wheel locally.
  5. Run twine check and inspect npm pack --dry-run --json.
  6. Commit and tag the release.
  7. Push the tag to GitHub.
  8. Publish npm and PyPI only after the local artifacts and checks are clean.

Publishing is intentionally not performed by ordinary development commands. Keep registry tokens and credentials outside the repository.

Security and privacy

  • Grudge is local-first and does not require telemetry.
  • Captured content is redacted before it is persisted where configured.
  • Runtime hook failures are designed to fail open with a visible warning rather than silently blocking the host agent.
  • Review .grudge/ and backup contents before sharing a workspace.
  • Never commit API keys, tokens, private keys, personal settings, or captured sensitive data.

Read the trust model before enabling capture in a production repository.

Roadmap

The current MVP intentionally leaves several deeper integrations explicit:

  • persistent Scar registry and retirement workflow;
  • changed-Scar selection;
  • fault localization and richer outcome predicates;
  • fuzzer and additional recorder adapters;
  • pytest and GitHub workflow integrations;
  • MCP and LangGraph adapters;
  • full viewer-backed Scar browser.

See docs/roadmap.md for the maintained roadmap.

Benchmark status

Two things are measured today. Neither is a general benchmark, and this section does not claim one.

1. Guard blocking. grudge verify runs the full chain in a temporary project on Windows, macOS, and Linux, and prints Result: PASS (10/10 checks) when a matching Bash call exits 2 and a benign one exits 0. That is the only proof of enforcement in the product.

2. Shrinking. The row below is a measured offline CLI run. The command path is documented in docs/demo-guide.md:

python -m grudge capture examples.leaky_agent.agent --out failure.jsonl
python -m grudge shrink failure.jsonl \
  --failure 'never flow read_file to send_http field body' \
  --agent examples.leaky_agent.agent --out cassette.json --json

Wall time includes capture and shrink, measured over three consecutive runs (4.68 s, 4.83 s, 5.31 s). Repeat on another machine before comparing timings.

Workload Trace steps captured Cassette entries Oracle calls Wall time Cassette bytes
Demo leak (offline, deterministic; Windows 11 / Python 3.14.0) 71 23 → 23 1,254 4.83 s 11,854 → 2,081

Entry reduction kept all 23 entries here because every recorded tool call is needed to replay the leak; shrinking removed content instead, cutting the cassette from 11,854 to 2,081 bytes (82%). The visual demo starts from the checked-in compact Scar and shrinks its world from 4 entries to 3. Entry counts do not fall dramatically on a leak that genuinely needs every step, so treat "fewer steps" as workload-dependent and this table as the one measured example.

The savings shown by grudge stats, statusline, and report are labelled estimated: they value a blocked call at a fixed token estimate rather than measuring a real retry.

Limits

These are deliberate boundaries, not unfinished work:

  • Taint matching is substring-based. tainted_by looks for the source value inside later tool arguments. It does not build a structured data-flow graph, so encoded or reformatted copies of a secret can evade it.
  • The Node runtime is a subset of the Python engine. The Node Guard evaluates call-shape, ordering, and taint-lite predicates. Shrinking, admission, statistics, and the demo run only in Python. A DSL predicate that needs the full Python compiler is not enforced by the Node hook.
  • The gate is offline and deterministic. It replays recorded cassettes and never calls a model provider. It catches regressions in policy code and tool arguments; it cannot detect drift caused only by a model change.
  • Advisory agents are not enforced. Codex, Gemini CLI, GitHub Copilot, OpenCode, and Windsurf receive an instruction file. Nothing blocks their tool calls, so compliance depends on the model reading the instructions.
  • Cursor writes nothing. The adapter is unverified and intentionally no-write.
  • Measurements are per environment. The benchmark row and hook timings were measured on one host; re-measure before quoting them elsewhere.

Contributing

Small, focused changes are preferred. Before opening a pull request:

python -m pytest -q
npm test --prefix packages/grudge
python -m scripts.docs_check
git diff --check

Please include the behavior you changed, the checks you ran, and any platform limitations that remain. Do not remove tests to make a check pass.

See CONTRIBUTING.md for the checks to run, what makes a good pull request, and the roadmap areas that most need help.

License

Grudge is released under the MIT License.

Copyright © 2026 Grudge contributors.

About

every failed agent trace is automatically turned into a regression test, so your agent never makes the same mistake twice

Topics

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages