errexplain
ee runs a command (or reads piped output), and when it fails it pulls out the error, reads only the source lines that matter from your disk, and asks Claude Haiku 5.5 for a short root cause and a concrete fix, as a unified diff when the model is confident. Most of the work (parsing, context selection, budgeting, redaction) is deterministic Python, so a request is typically a few hundred to ~3k input tokens, well under a cent.
Features
- Runs
ee -- CMD ...with live output (stdout and stderr merged), keeps the last 400 lines and the exit code, and exits with the wrapped command's exit code. Also works as a filter:cmd 2>&1 | ee. - Deterministic error parsers: Python tracebacks (including chained "During handling..."), pytest failure and collection-error sections with assertion rewrites, Node/TS stack traces,
tscerrors (plain and pretty), Go panics, compile errors and test failures, Rusterror[E0xxx]with-->locations and panics, Java/Kotlin stack traces (root cause first), GCC/Clang errors, plus a generic "last error-looking block" fallback. - Context builder: +-12 lines around each frame and the enclosing function (Python via
ast, other languages by an indentation heuristic), overlapping ranges merged, vendor frames (site-packages,node_modules,.venv) kept only when they are the top frame, budgeted to ~3k tokens with the top frames first. Adds OS, language version and declared dependency versions (requirements*.txt,pyproject.toml,package.json) for names imported in the snippets. - Safety: files outside the repo root are never read or sent,
.eeignoreblocks paths, and obvious secrets (API keys, tokens, JWTs, private keys,password=..., URL credentials) are redacted before anything leaves your machine. - Structured answer (summary, root cause, fix steps, optional patch, confidence, docs links) rendered with rich, with a syntax-highlighted diff.
--applyvalidates the patch withgit apply --checkand asks before applying.--dry-runshows exactly what would be sent; responses are cached on disk by sha256 of (model, prompt);--verboseshows tokens and estimated cost.
Install
cd errexplain
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
export PATH="$PWD/.venv/bin:$PATH" # provides `ee` and `errexplain`
export ANTHROPIC_API_KEY=sk-ant-... # or log in with `ant auth login`
Quickstart
ee -- pytest -x # run, and explain if it fails
make 2>&1 | ee # explain piped output
ee --dry-run -- python3 examples/pyapp/report.py # no key needed
ee --apply -- go build ./... # offer to apply the suggested patch
Usage
ee [OPTIONS] [COMMAND]...
--model TEXT Model id (env EE_MODEL) [default: claude-haiku-5-5]
--dry-run Print what would be sent and the token estimate; no API call.
-v, --verbose Show token usage and estimated cost.
--apply Apply the suggested patch (git apply --check first, asks to confirm).
-y, --yes Do not ask for confirmation with --apply.
--no-cache Ignore and do not write the response cache.
--root DIRECTORY Repo root; files outside it are never sent. [default: .]
--budget INTEGER Approximate token budget for source context. [default: 3000]
Flags that belong to the wrapped command are passed through (ee pytest -x -k foo); use -- if it clashes with an ee flag. A command that exits 0 is never analysed and never costs anything.
Example: dry run on a real failure
examples/pyapp/report.py looks up a SKU that is missing from prices.json. This is real output (system prompt omitted for brevity):
$ cd examples/pyapp && ee --dry-run -- python3 report.py
Traceback (most recent call last):
...
KeyError: 'C300'
ee dry run: model=claude-haiku-5-5, model not called
--- user ---
Command: python3 report.py
Exit code: 1
Detected: python / KeyError
Environment:
- OS: Linux 6.8.0-138-generic
- python: Python 3.12.3
Error output:
...
Source inventory.py lines 1-13:
9| def total_cost(items, prices):
10| total = 0
11| for item in items:
12| total += prices[item["sku"]] * item["qty"]
13| return total
Source report.py lines 1-12:
...
---
estimated input tokens: ~519 (~$0.000052)
The other samples in examples/ (Go panic, Node JSON.parse, Rust lifetime error, C undeclared identifier, a failing pytest; tsapp needs tsc, which was not available here) all work the same way, e.g. cd examples/goapp && ee --dry-run -- go run ..
Example: rendered answer (requires a real key)
The answer layout below was rendered from a stub response (the tests use a fake client); with a key the content comes from Haiku.
╭───────────────────────────────── errexplain ─────────────────────────────────╮
│ KeyError: no price for SKU C300. │
│ │
│ Root cause │
│ ITEMS in report.py contains C300, but prices.json only defines A100 and │
│ B200, so the lookup in total_cost raises KeyError. │
│ │
│ Fix │
│ 1. Add C300 to prices.json, or │
│ 2. Validate SKUs in total_cost and fail with a clear message. │
╰────────────────────────────── confidence: high ──────────────────────────────╯
--- a/inventory.py
...
tokens: 1000 in / 200 out, est. cost $0.000200
Configuration
| Setting | Purpose |
|---|---|
ANTHROPIC_API_KEY |
Credentials (never logged; there is no --api-key flag). |
EE_MODEL |
Default model id (claude-haiku-5-5). |
EE_CACHE_DIR |
Cache location (default ~/.cache/errexplain). |
.eeignore |
In the repo root; one glob or directory per line, # comments. Matching files are never read or sent. |
Architecture
| Module | Role |
|---|---|
runner.py |
Subprocess runner with tee, 400-line tail, exit codes (126/127 for launch failures); stdin reader. |
parsers.py |
Per-language error extraction into ErrorInfo (message, excerpt, frames ordered most relevant first). |
context.py |
Frame resolution, root and .eeignore enforcement, function spans, merging, token budget, environment facts. |
redact.py |
Secret masking applied to the whole prompt. |
llm.py |
Prompt builder, Explanation schema, messages.parse call, disk cache, error mapping, cost. |
patch.py |
git apply --check / apply with path-escape protection. |
render.py |
rich panel and diff. |
cli.py |
click command wiring. |
See docs/design.md for the algorithms.
Tests
.venv/bin/python -m pytest -q # 88 tests, no network or API key needed
Parsers run against real captured outputs in tests/fixtures/; the model is replaced by a fake client.
Limitations
- Live API calls were not exercised in development (no key available); the request shape follows the SDK's
messages.parsestructured-output API and is covered by fake-client tests. - The language version comes from the
python3/node/... onPATH, which may differ from the interpreter the wrapped command used. - Only frames that appear in the error output are read: a pytest assertion failure in a test shows the test file but not the function under test unless it appears in a traceback.
- The non-Python function-span detection is an indentation heuristic and can miss unusual formatting; the +-12 line window is always included.
- Output is read through pipes, not a PTY, so programs that colourise only on a TTY print plain text (which suits parsing).
- Patches are model-generated;
--applyonly guarantees they apply cleanly, not that they are correct.