Skip to content
Conduit

skill

immunize

A curated pattern library that stops AI coding assistants from repeating common runtime errors. No API key. No LLM calls at runtime.

viditkbhatnagar/immunizecommunitylistedunreviewed

Set up

Skills are installed by placing their folder where your agent loads skills.

The download comes from github.com/viditkbhatnagar/immunize itself, not from our server, so what you get is exactly what its author published.

Install instructions come from the README

Skills do not use an MCP config. Follow the steps in the README below.

README

synced Oct 23, 2026

immunize

Curated, verified guardrails that stop AI coding assistants from repeating runtime errors.

Offline · Deterministic · Cross-tool · Team-shareable · No API key at runtime.

PyPI version Python versions License: Apache 2.0 CI Patterns Status


Table of Contents

  1. What is immunize?
  2. Why it exists
  3. How it compares
  4. Architecture at a glance
  5. End-to-end pipeline
  6. Quickstart
  7. The four trigger paths
  8. Pattern matching internals
  9. Verification harness
  10. Artifact injection
  11. Bundled pattern library
  12. CLI reference
  13. Configuration
  14. Project layout
  15. Data model
  16. Authoring new patterns
  17. State storage
  18. Security & privacy
  19. Continuous integration
  20. Roadmap
  21. Contributing
  22. License

What is immunize?

immunize is a Python CLI and pattern library that turns the runtime errors your AI coding assistant produces into verified, durable, cross-tool guardrails committed to your repository.

When a bash command fails inside Claude Code (or any shell wrapped via immunize run), immunize:

  1. Captures the failure payload.
  2. Matches it against a curated regex/heuristic library — no LLM call at runtime.
  3. Runs the matched pattern's pytest in a subprocess to confirm the fix is sound in this environment.
  4. Injects three artifacts into your repo:
    • A Claude Code skill (.claude/skills/immunize-<slug>/SKILL.md)
    • A Cursor rule (.cursor/rules/<slug>.mdc)
    • A pytest regression test (tests/immunized/<slug>/test_template.py)

Commit them. Your team — and every AI session anyone runs against the repo — picks the immunity up automatically.

Mental model. Your immune system remembers pathogens it has met. immunize does the same for AI coding assistants: once it sees an error, the AI never repeats that class of error in your project again.


Why it exists

AI assistants repeat the same mistakes every session. You fix the stale-closure bug in useEffect on Monday; by Wednesday the assistant has rewritten it in a different file. The model has no durable memory of what you corrected.

Existing answers each fall short:

ApproachLimitation
Claude Code memory / Cursor rulesSingle-tool. Per-user. Untested — no build-time check that the rule prevents the error it claims to.
"Don't do that again" in chatSurvives one session, dies on /clear or a new teammate joining.
Linters / type checkersCatch what their authors imagined — not what the AI specifically over-produces.
Sentry-style triagersDetect & diagnose, but emit no persistent rules other tools can read.

immunize makes a different bet: a small, curated, verified library of patterns covering the mistakes that matter, shipping artifacts in formats every assistant already reads, with a pytest that proves each fix works.


How it compares

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'primaryTextColor':'#0f172a', 'lineColor':'#94a3b8'}}}%%
flowchart LR
    classDef tool fill:#ffffff,stroke:#64748b,stroke-width:1px,color:#0f172a
    classDef hero fill:#1e3a8a,stroke:#0f172a,stroke-width:1.5px,color:#f8fafc
    classDef cap fill:#f1f5f9,stroke:#cbd5e1,stroke-width:1px,color:#0f172a

    subgraph Tools["Tools"]
        direction TB
        I["immunize"]:::hero
        CB["Cursor Bugbot"]:::tool
        CM["Claude Memory"]:::tool
        SS["Sentry Seer"]:::tool
        SA["Semgrep Assistant"]:::tool
        CR["CodeRabbit"]:::tool
    end

    subgraph Caps["Capabilities"]
        direction TB
        C1["SKILL.md emit"]:::cap
        C2["Cursor rule emit"]:::cap
        C3["Verified pytest"]:::cap
        C4["Cross-tool"]:::cap
        C5["Team-shareable via git"]:::cap
        C6["Zero API key at runtime"]:::cap
    end

    I --> C1 & C2 & C3 & C4 & C5 & C6
    CM --> C1
    CB --> C2
    SS --> C2
    SA --> C2
    CR --> C5

    style Tools fill:#f8fafc,stroke:#cbd5e1
    style Caps fill:#f8fafc,stroke:#cbd5e1

Only immunize ships all six at once. Verified regression tests are the structural moat — every other tool in this space stops at "we found a problem"; immunize proves the fix.


Architecture at a glance

Three layers separate concerns cleanly: triggers, the engine, and the artifacts that land in your repo.

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'primaryTextColor':'#0f172a', 'lineColor':'#64748b'}, 'flowchart': {'curve':'basis'}}}%%
flowchart TB
    classDef trigger fill:#ffffff,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef engine fill:#1e293b,stroke:#0f172a,stroke-width:1px,color:#f8fafc
    classDef store fill:#334155,stroke:#0f172a,stroke-width:1px,color:#f8fafc
    classDef artifact fill:#f1f5f9,stroke:#475569,stroke-width:1px,color:#0f172a

    subgraph L1["Layer 1 · Triggers"]
        direction LR
        T1["Claude Code hook<br/>PostToolUseFailure"]:::trigger
        T2["Shell wrapper<br/>immunize run &lt;cmd&gt;"]:::trigger
        T3["Bundled skill<br/>immunize-manager"]:::trigger
        T4["Manual CLI<br/>immunize capture"]:::trigger
    end

    subgraph L2["Layer 2 · Engine"]
        direction LR
        E1["capture.py"]:::engine --> E2["matcher.py"]:::engine
        E2 --> E3["verify.py"]:::engine
        E3 --> E4["inject.py"]:::engine
        E5[("SQLite<br/>.immunize/state.db")]:::store
        E1 -. persist .-> E5
        E4 -. record .-> E5
    end

    subgraph L3["Layer 3 · Generated artifacts"]
        direction LR
        A1[".claude/skills/immunize-&lt;slug&gt;/<br/>SKILL.md"]:::artifact
        A2[".cursor/rules/&lt;slug&gt;.mdc"]:::artifact
        A3["tests/immunized/&lt;slug&gt;/<br/>test_template.py"]:::artifact
    end

    L1 --> L2 --> L3

    style L1 fill:#f8fafc,stroke:#cbd5e1
    style L2 fill:#f8fafc,stroke:#cbd5e1
    style L3 fill:#f8fafc,stroke:#cbd5e1
  • Layer 1 is plural by design. A single failure can arrive from a Claude Code hook, a immunize run subprocess, the bundled skill nudging the model, or a manual JSON pipe.
  • Layer 2 is one pipeline: capture → match → verify → inject. Pure Python. No network, no API key.
  • Layer 3 is the durable output — committed to git, picked up automatically on git pull by every assistant the team uses.

End-to-end pipeline

The same five-step pipeline runs no matter which trigger fires:

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'actorBkg':'#1e293b', 'actorTextColor':'#f8fafc', 'actorLineColor':'#475569', 'signalColor':'#475569', 'signalTextColor':'#0f172a', 'labelBoxBkgColor':'#f1f5f9', 'labelBoxBorderColor':'#cbd5e1', 'labelTextColor':'#0f172a', 'noteBkgColor':'#f1f5f9', 'noteBorderColor':'#cbd5e1', 'noteTextColor':'#0f172a', 'sequenceNumberColor':'#f8fafc'}}}%%
sequenceDiagram
    autonumber
    participant Tool as AI tool / shell
    participant CLI as immunize CLI
    participant Match as matcher
    participant Verify as verify
    participant Inject as inject
    participant Repo as Repository
    participant DB as state.db

    Tool->>CLI: failure JSON on stdin (Bash exit ≠ 0)
    CLI->>DB: persist CapturePayload
    CLI->>Match: load_patterns and match
    Match-->>CLI: ranked MatchResult[]
    alt no pattern clears threshold
        CLI-->>Tool: outcome unmatched
    else top pattern clears
        CLI->>Verify: pytest in subprocess (swap fix → repro)
        alt verify fails
            CLI-->>Tool: outcome matched_verify_failed
        else verify passes
            CLI->>Inject: atomic write 3 artifacts
            Inject->>Repo: SKILL.md + cursor rule + test_template
            CLI->>DB: insert artifacts row
            CLI-->>Tool: outcome matched_and_verified
        end
    end

Stdout is a strict one-line JSON contract; Rich console output goes to stderr. Hook handlers can trust the shape.

Decision tree at the matcher

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'primaryTextColor':'#0f172a', 'lineColor':'#64748b'}}}%%
flowchart TD
    classDef step fill:#ffffff,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef gate fill:#f1f5f9,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef ok fill:#1e3a8a,stroke:#0f172a,stroke-width:1px,color:#f8fafc
    classDef drop fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#475569,stroke-dasharray:3 3

    A["CapturePayload arrives"]:::step --> B["Compile regex per pattern · cached by pattern.id"]:::step
    B --> C["Score = stderr + stdout + class hint + language"]:::step
    C --> D{"≥ per-pattern<br/>min_confidence?"}:::gate
    D -- no --> E["Drop"]:::drop
    D -- yes --> F{"≥ global floor<br/>IMMUNIZE_MIN_MATCH_CONFIDENCE?"}:::gate
    F -- no --> E
    F -- yes --> G["Sort desc by confidence"]:::step
    G --> H["Take top 1"]:::step
    H --> I["Verify in subprocess"]:::ok
    I -- pass --> J["Inject and persist"]:::ok
    I -- fail --> K["Emit matched_verify_failed"]:::drop
    E --> L["Emit unmatched"]:::drop

Each gate is documented in src/immunize/matcher.py and src/immunize/cli.py.


Quickstart

pip install immunize        # core wheel — zero LLM dependencies
cd your-project
immunize install-skill      # drops .claude/skills/immunize-manager/SKILL.md
immunize install-hook       # registers .claude/settings.json PostToolUseFailure hook

Restart Claude Code. From this point on, every failed Bash tool call inside Claude Code feeds immunize capture automatically. When a known error class hits — AttributeError: 'NoneType', ModuleNotFoundError, a 429 crash, a CORS preflight rejection — three files appear in your repo. Commit them.

Outside Claude Code (Cursor, plain shells, CI):

immunize run pytest tests/
immunize run npm test
immunize run python manage.py migrate

immunize run tees output live, propagates exit codes, and on non-zero exit feeds the same matcher pipeline.

Windows note. A user-scope pip install immunize puts immunize.exe in %APPDATA%\Python\Python<ver>\Scripts\, which is not on PATH by default. The hook command Claude Code runs is the bare immunize capture …, so the hook can't reach the executable until that Scripts directory is on PATH. The cleanest fix is to install inside a venv (py -m venv .venv && .venv\Scripts\activate && pip install immunize); the next-cleanest is adding the Scripts directory to user PATH (System Properties → Environment Variables, or setx PATH "%PATH%;%APPDATA%\Python\Python<ver>\Scripts"). Verify with where immunize before running install-hook.


The four trigger paths

Four entry points, one engine. Picking the right one is just a matter of where your failures originate.

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'primaryTextColor':'#0f172a', 'lineColor':'#64748b'}}}%%
flowchart LR
    classDef src fill:#ffffff,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef trg fill:#f1f5f9,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef eng fill:#1e3a8a,stroke:#0f172a,stroke-width:1.5px,color:#f8fafc

    subgraph In["Where the failure happens"]
        direction TB
        F1["Claude Code · Bash tool"]:::src
        F2["Cursor / plain shell"]:::src
        F3["Claude Code · no hook configured"]:::src
        F4["Existing log or scripted replay"]:::src
    end

    subgraph Triggers["Trigger path"]
        direction TB
        T1["PostToolUseFailure hook<br/>--source claude-code-hook"]:::trg
        T2["immunize run &lt;cmd&gt;<br/>--source shell-wrapper"]:::trg
        T3["immunize-manager skill<br/>nudges Claude to call"]:::trg
        T4["echo '{...}' &#124; immunize capture<br/>--source manual"]:::trg
    end

    F1 --> T1
    F2 --> T2
    F3 --> T3
    F4 --> T4

    T1 & T2 & T3 & T4 --> ENGINE[("capture → match → verify → inject")]:::eng

    style In fill:#f8fafc,stroke:#cbd5e1
    style Triggers fill:#f8fafc,stroke:#cbd5e1
PathSet up byStreams output?Best for
Hookimmunize install-hookn/a (Claude shows it)Zero-touch automation in Claude Code
immunize runnothing — invoke per commandYes (live tee)CI, Cursor, bare shells
Bundled skillimmunize install-skilln/aFalls back to immunize run when no hook
Manual capturehand-built JSONn/aReplays, scripted captures, testing

The hook payload is translated by payload_from_claude_code_hook into a CapturePayload — non-Bash failures are skipped cleanly, never producing noisy unmatched captures.


Pattern matching internals

A Pattern is a five-file directory under src/immunize/patterns/<slug>/ and a YAML descriptor. The matcher scores each candidate by combining four signals:

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'primaryTextColor':'#0f172a', 'lineColor':'#64748b'}}}%%
flowchart LR
    classDef input fill:#1e293b,stroke:#0f172a,stroke-width:1px,color:#f8fafc
    classDef signal fill:#ffffff,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef agg fill:#f1f5f9,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef gate fill:#ffffff,stroke:#1e3a8a,stroke-width:1.5px,color:#0f172a
    classDef ok fill:#1e3a8a,stroke:#0f172a,stroke-width:1px,color:#f8fafc
    classDef drop fill:#f8fafc,stroke:#94a3b8,color:#475569,stroke-dasharray:3 3

    P["CapturePayload<br/>stderr + stdout"]:::input --> S1["stderr regex<br/>min 0.6, +0.3 each"]:::signal
    P --> S2["stdout regex<br/>min 0.4, +0.2 each"]:::signal
    P --> S3["error class hint<br/>+0.15 if match"]:::signal
    P --> S4["language detection<br/>+0.10 if pattern lang ∈ detected"]:::signal
    S1 & S2 & S3 & S4 --> SUM[["Σ capped at 1.0"]]:::agg
    SUM --> CMP{"≥ pattern.min_confidence<br/>and global floor?"}:::gate
    CMP -- yes --> RANK["ranked candidate"]:::ok
    CMP -- no --> DROP["dropped"]:::drop

Implementation references:

  • Score breakdown — score_pattern
  • Error class keyword set — ERROR_CLASS_HINTS (cors, import, auth, rate_limit, type_error, null_ref, config, network)
  • Language signatures — _LANGUAGE_SIGNATURES (python, javascript, typescript, go, rust)
  • Word-bounded keyword regex — fixes the latent collision where ENOTFOUND substring-matched inside ModuleNotFoundError.
  • Float epsilon (1e-9) — defends 0.3 + 0.15 ≥ 0.45 from IEEE-754 imprecision.

Two thresholds, one authoritative source

LayerFieldDefaultPurpose
Per-patternpattern.match.min_confidencevaries (0.30–0.50)Authored against real-world stderr
Global floorSettings.min_match_confidence0.30Operator escape hatch (raise via IMMUNIZE_MIN_MATCH_CONFIDENCE for CI strict mode)

Pre-v0.2.0 the global floor was 0.70 and silently shadowed every per-pattern threshold below it; calibration data and the rationale live in _planning/MATCHER_CALIBRATION_V020.md.


Verification harness

A pattern only ships if its pytest fails without the fix and passes with it. The harness re-checks the passes-with-fix half on the user's machine before injecting, catching environment drift (missing optional deps, pytest version skew):

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'primaryTextColor':'#0f172a', 'lineColor':'#64748b'}}}%%
flowchart TB
    classDef step fill:#ffffff,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef gate fill:#f1f5f9,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef ok fill:#1e3a8a,stroke:#0f172a,stroke-width:1.5px,color:#f8fafc
    classDef bad fill:#f8fafc,stroke:#94a3b8,color:#475569,stroke-dasharray:3 3

    Start(["verify.verify pattern"]):::ok --> Has{"fixtures/ has<br/>repro.* + fix.* pair?"}:::gate
    Has -- no --> Run["Run pytest as-is"]:::step
    Has -- yes --> Backup["Read repro.* bytes into memory"]:::step
    Backup --> Swap["Write fix.* bytes over repro.* path"]:::step
    Swap --> Run
    Run --> Sub["python -m pytest -x -q<br/>cwd = pattern dir · timeout = 30s"]:::step
    Sub --> Code{"exit code?"}:::gate
    Code -- "0" --> Pass(["passed = True"]):::ok
    Code -- "1, 2, 5" --> Fail(["passed = False<br/>+ truncated diagnostic"]):::bad
    Code -- "TimeoutExpired" --> TO(["passed = False<br/>error: pytest timed out"]):::bad
    Sub --> Restore["finally: write original repro.* bytes back"]:::step
    Restore --> Pass
    Restore --> Fail
    Restore --> TO

Reference: verify.verify (user-runtime swap), scripts/pattern_lint.py (CI dual-run: FAIL → PASS → FAIL-after-restore).

The CI path is stricter than runtime — it runs the full three-phase swap so a broken pattern can never be merged in the first place.


Artifact injection

Three files land in fixed paths inside the user's project, each via an atomic write (PID-suffixed temp + os.replace):

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'primaryTextColor':'#0f172a', 'lineColor':'#64748b'}}}%%
flowchart LR
    classDef src fill:#1e293b,stroke:#0f172a,stroke-width:1px,color:#f8fafc
    classDef art fill:#ffffff,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef out fill:#f1f5f9,stroke:#475569,stroke-width:1px,color:#0f172a

    Pattern[("src/immunize/patterns/&lt;slug&gt;/")]:::src
    Pattern --> SKILL["SKILL.md"]:::art --> O1[".claude/skills/immunize-&lt;slug&gt;/SKILL.md"]:::out
    Pattern --> CR["cursor_rule.mdc"]:::art --> O2[".cursor/rules/&lt;slug&gt;.mdc"]:::out
    Pattern --> TEST["test_template.py"]:::art --> O3["tests/immunized/&lt;slug&gt;/test_template.py"]:::out
    Pattern --> FIX["fixtures/repro.* + fix.*"]:::art --> O4["tests/immunized/&lt;slug&gt;/fixtures/<br/>(repro slot receives FIX bytes)"]:::out
    Pattern --> SG["semgrep.yml · optional"]:::art --> O5[".semgrep/&lt;slug&gt;.yml"]:::out

Two subtleties matter and are guarded by tests:

  1. Repro-slot rewrite. test_template.py reads fixtures/repro.*. If we copied the pattern's buggy repro verbatim, the injected guardrail would fail against itself. _copy_fixtures_with_repro_rewrite writes the fix bytes into the repro path — keeping the test's path expression unchanged while shipping a passing regression test. Source-tree repros stay intact for pattern_lint.
  2. Slug collision. resolve_slug probes 99 candidates (base, base-2, …, base-99) against both the SQLite artifacts table and the on-disk paths. If exhausted, SlugExhaustedError instructs the user to run immunize remove.

Bundled pattern library

Eleven patterns ship as of the latest main. The original seven were calibrated against 35+ real-world stderr samples; per-pattern recall details and the false-positive audit live in _planning/MATCHER_CALIBRATION_V020.md.

IDLanguagesClassThresholdCatches
react-hook-missing-depjs, tslint0.30useEffect/useCallback/useMemo referencing reactive values not in deps array (matches the canonical react-hooks/exhaustive-deps rule ID)
fetch-missing-credentialsjs, tscors0.45Cross-origin authenticated fetch without credentials: 'include' (Chrome + Firefox phrasings)
python-none-attribute-accesspythonnull_ref0.30AttributeError: 'NoneType' object has no attribute … and TypeError: 'NoneType' object is not subscriptable
import-not-found-pythonpythonimport0.50ModuleNotFoundError and ImportError: cannot import name
missing-env-varpythonconfig0.40KeyError: 'UPPER_SNAKE' from os.environ['…'] access
rate-limit-no-backoffpythonrate_limit0.45429 Too Many Requests, RateLimitError, HTTPError … 429, rate_limit_error SDK responses
async-fn-called-without-awaitpythonasync0.30coroutine '<name>' was never awaited
timezone-naive-datetimepythontype_error0.40TypeError: can't compare/subtract offset-naive and offset-aware datetimes (e.g., comparing datetime.now() with an HTTP API timestamp)
node-cjs-esm-mismatchjs, tsmodule0.30Node module mixes CommonJS require() with ES Module syntax — Cannot use import statement outside a module and require is not defined in ES module scope
promise-unhandled-rejectionjs, tsasync0.30Promise chain or await with no rejection handler — UnhandledPromiseRejection, Unhandled promise rejection, Uncaught (in promise) …
json-decode-no-handlingpythonparse0.40json.loads() / response.json() on untrusted input without try/except — leaks raw json.decoder.JSONDecodeError across the API boundary

Each directory contains:

<slug>/
├── pattern.yaml         # metadata + match rules + verification config
├── SKILL.md             # Claude Code skill (frontmatter: name, description)
├── cursor_rule.mdc      # Cursor rule  (frontmatter: description, globs, alwaysApply)
├── test_template.py     # pytest assertion that proves the fix
└── fixtures/
    ├── repro.<ext>      # buggy form  — test fails against this
    └── fix.<ext>        # correct form — test passes against this

CLI reference

immunize ships eight commands. Help on each: immunize <cmd> --help.

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'primaryTextColor':'#0f172a', 'lineColor':'#94a3b8'}}}%%
flowchart LR
    classDef capture fill:#1e3a8a,stroke:#0f172a,stroke-width:1px,color:#f8fafc
    classDef mgmt fill:#334155,stroke:#0f172a,stroke-width:1px,color:#f8fafc
    classDef setup fill:#0f766e,stroke:#0f172a,stroke-width:1px,color:#f8fafc
    classDef contrib fill:#475569,stroke:#0f172a,stroke-width:1px,color:#f8fafc

    subgraph Cap["Capture"]
        direction TB
        capture["capture"]:::capture
        run["run"]:::capture
    end

    subgraph Manage["Manage"]
        direction TB
        list["list"]:::mgmt
        verify["verify"]:::mgmt
        remove["remove"]:::mgmt
    end

    subgraph Setup["Setup"]
        direction TB
        install_skill["install-skill"]:::setup
        install_hook["install-hook"]:::setup
    end

    subgraph Contribute["Contribute"]
        direction TB
        author_pattern["author-pattern"]:::contrib
    end

    style Cap fill:#f8fafc,stroke:#cbd5e1
    style Manage fill:#f8fafc,stroke:#cbd5e1
    style Setup fill:#f8fafc,stroke:#cbd5e1
    style Contribute fill:#f8fafc,stroke:#cbd5e1
CommandPurpose
immunize capture [--source S] [--stdin-plain] [--dry-run]Match → verify → inject from a JSON or plain-stderr stdin payload. Emits one JSON line on stdout.
immunize run [--timeout N] [--no-capture] -- <cmd> [args]Spawn <cmd>, tee output live, propagate exit code, auto-capture on non-zero exit.
immunize listTable of every immunity active in the project.
immunize verify [<id|slug>]Re-run pytest against one (or all) injected immunities.
immunize remove <id|slug> [--yes]Delete artifacts and SQLite row for an immunity.
immunize install-skill [--project-dir PATH] [--force]Copy bundled immunize-manager skill into .claude/skills/.
immunize install-hook [--project-dir PATH] [--force]Register the PostToolUseFailure hook in .claude/settings.json.
immunize author-pattern --from-error E.json --output DIR [--model M]Contributor-only. Drafts a new bundled pattern via Claude API; lives behind pip install 'immunize[author]'.

immunize capture JSON output contract

Exactly one of these shapes on stdout (always exit 0 unless the CLI itself is broken):

// matched + verified → 3 files written
{
  "outcome": "matched_and_verified",
  "matched": true,
  "verified": true,
  "pattern_id": "python-none-attribute-access",
  "pattern_origin": "bundled",
  "confidence": 0.55,
  "artifacts": {
    "skill":       "/abs/.claude/skills/immunize-python-none-attribute-access/SKILL.md",
    "cursor_rule": "/abs/.cursor/rules/python-none-attribute-access.mdc",
    "pytest":      "/abs/tests/immunized/python-none-attribute-access/test_template.py"
  }
}

// matched but verify failed in this env (e.g. pytest missing) → no files written
{ "outcome": "matched_verify_failed", "matched": true, "verified": false,
  "pattern_id": "...", "pattern_origin": "bundled",
  "confidence": 0.55, "reason": "pytest is not installed in this environment. Run: pip install pytest" }

// no pattern cleared threshold
{ "outcome": "unmatched", "matched": false, "can_author_locally": true }

// hook fired but it wasn't a Bash failure
{ "outcome": "skipped", "reason": "non-Bash tool failure", "tool_name": "Edit" }

Rich-formatted, human-readable output goes to stderr. Stdout is for machines.


Configuration

Settings resolve highest-priority first:

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'primaryTextColor':'#0f172a', 'lineColor':'#64748b'}}}%%
flowchart TB
    classDef src fill:#ffffff,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef merge fill:#f1f5f9,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef out fill:#1e3a8a,stroke:#0f172a,stroke-width:1.5px,color:#f8fafc

    A["CLI flags"]:::src --> M{"merge"}:::merge
    B["Environment vars · IMMUNIZE_*"]:::src --> M
    C[".immunize/config.toml"]:::src --> M
    D["~/.config/immunize/config.toml"]:::src --> M
    E["Built-in defaults"]:::src --> M
    M --> S["Settings"]:::out
SettingTOML keyEnv varDefault
LLM model (authoring only)modelIMMUNIZE_MODELclaude-sonnet-4-6
Verify timeout (seconds)[verify] timeout_secondsIMMUNIZE_VERIFY_TIMEOUT_SECONDS30
Verify retry count[verify] retry_countIMMUNIZE_VERIFY_RETRY_COUNT1
Global match floor[match] min_confidenceIMMUNIZE_MIN_MATCH_CONFIDENCE0.30
Local patterns dir[match] local_patterns_dirIMMUNIZE_LOCAL_PATTERNS_DIR.immunize/patterns_local/
Generate semgrep YAML[generate] semgrepIMMUNIZE_GENERATE_SEMGREPfalse

Two operational toggles:

  • IMMUNIZE_DEBUG_HOOK=1 — dump every Claude Code hook payload to .immunize/hook_payloads/<ts>-<session>.json for offline inspection (auto-gitignored).
  • IMMUNIZE_MIN_MATCH_CONFIDENCE=0.6 — global floor knob; useful in CI strict mode.

Project layout

immunize/
├── pyproject.toml                       # hatchling build, runtime deps, [author]/[dev] extras
├── README.md                            # this file
├── CHANGELOG.md                         # Keep-a-Changelog formatted history
├── CONTRIBUTING.md                      # dev setup + pattern authoring workflow
├── LICENSE                              # Apache-2.0
├── Makefile                             # install / test / lint / format / build / clean
├── .pre-commit-config.yaml              # ruff (check + format)
├── .github/workflows/
│   ├── ci.yml                           # pytest matrix (3.10/3.11/3.12) + pattern_lint
│   └── release.yml                      # tag-driven PyPI publish via OIDC
├── _planning/                           # design docs (architecture, spec, calibration, …)
├── scripts/
│   └── pattern_lint.py                  # CI gate: structural + behavioral pattern check
├── src/immunize/
│   ├── __init__.py                      # __version__
│   ├── __main__.py                      # `python -m immunize`
│   ├── cli.py                           # Typer app + 8 commands
│   ├── capture.py                       # stdin parsing, hook translation, fingerprinting
│   ├── matcher.py                       # regex compile + score + threshold
│   ├── verify.py                        # pytest subprocess + fix→repro swap
│   ├── inject.py                        # atomic writes + slug collision resolution
│   ├── runner.py                        # `immunize run` subprocess wrapper (tee threads)
│   ├── hook_installer.py                # idempotent merge into .claude/settings.json
│   ├── skill_install.py                 # copy bundled skill into project
│   ├── storage.py                       # SQLite schema, in-place migration, queries
│   ├── config.py                        # toml + env + CLI override merge
│   ├── models.py                        # Pydantic v2: CapturePayload, Pattern, Settings, …
│   ├── authoring/
│   │   └── cli_author.py                # `author-pattern` (lazy-imports anthropic)
│   ├── patterns/                        # 11 bundled patterns (slug-named dirs)
│   └── skill_assets/
│       └── immunize-manager/SKILL.md    # bundled skill
└── tests/                               # 19 test modules covering every module

Data model

Pydantic v2 models in src/immunize/models.py define the contract:

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'primaryColor':'#f1f5f9', 'primaryBorderColor':'#475569', 'primaryTextColor':'#0f172a', 'lineColor':'#64748b'}}}%%
classDiagram
    direction LR
    class CapturePayload {
        +Source source
        +str|None tool_name
        +str|None command
        +str stdout
        +str stderr
        +int exit_code
        +str cwd
        +datetime timestamp
        +str project_fingerprint
        +str|None session_id
    }

    class Pattern {
        +str id «kebab-case ≤40»
        +int version
        +int schema_version
        +str author
        +Literal origin «bundled|local|community»
        +str error_class
        +list~str~ languages
        +str description
        +MatchRules match
        +Verification verification
        +Path|None directory
    }

    class MatchRules {
        +list~str~ stderr_patterns
        +list~str~ stdout_patterns
        +str|None error_class_hint
        +float min_confidence
    }

    class Verification {
        +str pytest_relative_path
        +bool expected_fail_without_fix
        +bool expected_pass_with_fix
        +int timeout_seconds
    }

    class MatchResult {
        +Pattern pattern
        +float confidence
        +list~str~ matched_stderr_patterns
        +list~str~ matched_stdout_patterns
        +dict score_breakdown
    }

    class Settings {
        +str model
        +bool generate_semgrep
        +int verify_timeout_seconds
        +int verify_retry_count
        +Path project_dir
        +Path state_db_path
        +float min_match_confidence
        +Path|None local_patterns_dir
    }

    class VerificationResult {
        +bool passed
        +str|None error_message
    }

    Pattern *-- MatchRules
    Pattern *-- Verification
    MatchResult o-- Pattern

Source enum: "claude-code-hook" | "shell-wrapper" | "manual". The CLI rejects any other value before persisting.


Authoring new patterns

Two paths produce the same five-file directory shape on disk:

Path 1 — LLM-assisted (contributor only)

pip install 'immunize[author]'
export ANTHROPIC_API_KEY=sk-ant-...
immunize author-pattern \
  --from-error path/to/error.json \
  --output src/immunize/patterns/

Internally:

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'actorBkg':'#1e293b', 'actorTextColor':'#f8fafc', 'actorLineColor':'#475569', 'signalColor':'#475569', 'signalTextColor':'#0f172a', 'labelBoxBkgColor':'#f1f5f9', 'labelBoxBorderColor':'#cbd5e1', 'labelTextColor':'#0f172a', 'noteBkgColor':'#f1f5f9', 'noteBorderColor':'#cbd5e1', 'noteTextColor':'#0f172a'}}}%%
sequenceDiagram
    participant CLI as author-pattern
    participant API as Anthropic API
    participant Tmp as scratch dir
    participant Lint as pattern_lint

    CLI->>API: 1. analysis call (slug + error_class + languages + regex + confidence)
    API-->>CLI: tool_use response (forced JSON)
    CLI->>API: 2. drafting call (skill, cursor rule, pytest, repro, fix)
    API-->>CLI: tool_use response
    CLI->>Tmp: write 5 files
    CLI->>Lint: subprocess pattern_lint --patterns-dir scratch
    alt verifier rejects
        CLI->>API: 3. retry drafting with prior errors injected
        API-->>CLI: revised draft
        CLI->>Tmp: rewrite 5 files
        CLI->>Lint: re-run
    end
    alt still failing
        CLI->>CLI: dump to .immunize/rejected/<slug>/
    else passing
        CLI->>CLI: shutil.move scratch → output/<slug>/
    end

The Anthropic SDK is only imported here. End-user installs of immunize never pull anthropic, httpx, or jiter — it is gated behind the [author] extra.

Path 2 — manual

Copy an existing pattern (e.g. python-none-attribute-access), adapt the YAML, SKILL, rule, test, and fixtures. Then:

python scripts/pattern_lint.py

pattern_lint enforces the Ten Commandments of pattern quality (full text in _planning/PATTERN_AUTHORING.md). The most consequential:

  1. A pattern earns its slug. Generic names rejected.
  2. Patterns test defaults, not knobs.
  3. repro.* and fix.* must differ observably under the same test.
  4. Stderr regex anchors on signal, not noise.
  5. min_confidence is honest.
  6. SKILL.md teaches, doesn't lecture.
  7. No URLs in SKILL.md.
  8. Languages list is precise.
  9. Never wrap domain knowledge.
  10. If the test is flaky, the pattern is wrong.

State storage

A SQLite database at <project>/.immunize/state.db tracks captures and injections. Schema (see storage.py):

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'lineColor':'#475569', 'primaryColor':'#f1f5f9', 'primaryBorderColor':'#475569', 'primaryTextColor':'#0f172a'}}}%%
erDiagram
    errors ||--o{ artifacts : "fingerprints"
    errors {
        int id PK
        text payload_json
        text captured_at
        text project_fingerprint
    }
    artifacts {
        int id PK
        text pattern_id
        text pattern_origin
        text slug
        text skill_path
        text cursor_rule_path
        text semgrep_path
        text pytest_path
        int verified
        text created_at
    }
    diagnoses {
        int id PK
        int error_id FK
        text diagnosis_json
        text model
        text created_at
    }
    rejections {
        int id PK
        int diagnosis_id FK
        text reason
        text rejected_at
    }

diagnoses survives from the pre-v0.1 LLM-at-runtime architecture for backward compat; the matcher pipeline writes only to errors and artifacts. An in-place migration in _migrate_artifacts_if_needed upgrades legacy schemas on first connect.

The DB is local state only — auto-.gitignored via the install-hook flow. Shared state is the committed artifact files.


Security & privacy

ConcernStance
LLM at runtimeNever. The matcher is regex; verify is pytest in subprocess; inject is os.replace. The Anthropic SDK is unreachable from the capture import graph.
Network callsNever at user-runtime. author-pattern (contributor) is the sole exception, gated behind the [author] extra and an explicit ANTHROPIC_API_KEY.
Stderr exfiltrationStderr is persisted only to local SQLite under .immunize/. Hook payload dumps are off by default and gitignored.
Atomic writesEvery artifact write is PID-suffixed temp + os.replace. Concurrent immunize processes (e.g. a hook firing during a manual capture) never observe a partial file.
Hook scopeWe modify project-scope .claude/settings.json only, never user-scope ~/.claude/settings.json (a global hook would fire in unrelated projects).
Idempotenceinstall-hook and install-skill are no-ops on identical re-invocation; --force is required to overwrite drifted entries.
OS supportmacOS, Linux, and Windows. The full pipeline (capture → match → verify → inject) and CLI run on all three; CI exercises every supported Python version on Linux + Windows.

Continuous integration

%%{init: {'theme':'base', 'themeVariables': {'fontFamily':'Inter, system-ui, -apple-system, sans-serif', 'primaryTextColor':'#0f172a', 'lineColor':'#64748b'}}}%%
flowchart LR
    classDef trig fill:#ffffff,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef job fill:#f1f5f9,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef step fill:#ffffff,stroke:#475569,stroke-width:1px,color:#0f172a
    classDef ok fill:#1e3a8a,stroke:#0f172a,stroke-width:1.5px,color:#f8fafc

    Push["push / PR"]:::trig --> CI["ci.yml"]:::job
    Tag["tag v*"]:::trig --> Rel["release.yml"]:::job
    CI --> Test["matrix 3.10 / 3.11 / 3.12<br/>pytest + ruff"]:::step
    CI --> PLint["pattern_lint.py<br/>FAIL → PASS → FAIL-after-restore"]:::step
    Test & PLint --> Green(("green")):::ok
    Rel --> Build["python -m build"]:::step
    Build --> Pub["pypa/gh-action-pypi-publish<br/>OIDC trusted publisher"]:::step
  • ci.yml — installs [dev] extras, runs ruff check ., pytest, and python scripts/pattern_lint.py on three Python versions in parallel.
  • release.yml — fires on v* tag pushes, builds wheel + sdist, and publishes to PyPI via OIDC trusted publishing (no API tokens stored).

Roadmap

Tracked in _planning/LAUNCH_LIBRARY.md and CHANGELOG entries.

VersionThemeStatus
v0.1.xCore pipeline, 7 bundled patterns, manual captureshipped
v0.2.xClaude Code hook automation, immunize run, calibrated matchercurrent (latest tag v0.2.1)
main (post-v0.2.1)4 new patterns: timezone-naive-datetime, node-cjs-esm-mismatch, promise-unhandled-rejection, json-decode-no-handling (library now 11)merged, awaiting release
v0.3.xNative test runners (Jest/Go/cargo), more patternsplanned
v0.4+Community pattern registry, IDE-side telemetry opt-inexploratory

Explicit non-goals: web dashboard, MCP server, always-on shell daemon, IDE extensions. The premise is to emit files the existing tools already read.


Contributing

Contributions — especially new patterns — are warmly welcomed. Start with CONTRIBUTING.md for dev setup and _planning/PATTERN_AUTHORING.md for the Ten Commandments.

git clone https://github.com/viditkbhatnagar/immunize.git
cd immunize
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pre-commit install
make test                              # full pytest suite
python scripts/pattern_lint.py         # validate every bundled pattern

PRs land via GitHub. One pattern per PR. CI must be green.


License

Apache-2.0 — see LICENSE.

Built with Python 3.10+, Typer, Pydantic v2, Rich, and pytest. No LangChain. No daemons. No telemetry.

↑↓ navigate openesc close