Run ruff + bandit over the target, then judge every hit: is this silence
deliberate and correct, or is an error going missing with nobody the wiser?
A plain linter flags except Exception: pass the same way whether it's a
documented, narrow, best-effort suppression or a lazy catch-all hiding a real
bug — the judgment pass here is what tells them apart.
Two passes. audit.py in this skill's directory is pass one —
mechanical: run ruff and bandit, parse their JSON output into findings rows.
It never judges justified vs unjustified; every row it emits starts bucket: fix (a caught-and-dropped exception is a real smell by construction — same
reasoning duplication's parser uses for a token clone). The judgment pass
below reads those rows and reassigns the real bucket.
Buckets & categories
fix — the silence is unjustified: no re-raise, no log, no comment
explaining why swallowing is correct here. Make it fail loud — log it,
re-raise it, or narrow the caught type.
justified — the silence is deliberate and correct: a documented
reason (a comment naming why this failure is expected and safe to ignore),
a genuinely-optional cleanup, or contextlib.suppress with a narrow named
exception and a comment. Leave it.
unsure — flagged, and it isn't a clean fit for either bucket above:
no comment, but the surrounding code makes the intent ambiguous rather than
clearly careless.
category is the tool code slugged into a small closed set: bare-except
(ruff BLE001), try-except-pass (ruff SIM105 / bandit B110),
try-except-continue (bandit B112), raise-without-from (ruff B904),
try-consider (any ruff TRY0xx). When more than one code fires on the same
line, bare-except > raise-without-from > try-except-continue >
try-except-pass > try-consider picks the row's category; extra.codes
lists every code that fired, so pass two sees the full mechanical picture.
Run
Scope tight. Audit $ARGUMENTS if given; with no argument, scope
defaults per ~/.agents/skills/all-audits/SKILL.md's Scope section. Skip
vendored, generated, and dependency trees (node_modules, dist, .venv,
vendor, build output, lockfiles) and any .git/ or worktrees/ tree.
Pass one — run ruff and bandit over the SAME absolute scope, parse
them.
scope="$(realpath "${ARGUMENTS:-.}")"
uvx ruff check --select BLE,TRY,B904,SIM105 --output-format json "$scope" > /tmp/ruff-out.json
uvx bandit -r "$scope" -f json -t B110,B112 -q \
--exclude "$scope/node_modules,$scope/.venv,$scope/dist,$scope/vendor,$scope/.git,$scope/build,$scope/worktrees" \
> /tmp/bandit-out.json
python3 ~/.agents/skills/error-handling/audit.py /tmp/ruff-out.json /tmp/bandit-out.json
Using the same absolute path for both tools matters: ruff's JSON always
reports absolute filenames; bandit's mirrors whatever scope you gave it.
Different scope forms mean different filename strings, and same-line hits
from the two tools won't merge into one row.
audit.py's parse_findings(ruff_json, bandit_json) -> list[dict] is the
tested seam (~/.agents/skills/error-handling/fixtures/ + answer-key.md back it,
mirroring ~/.agents/skills/dead-code/fixtures/) — pure, no subprocess inside it, fed both
tools' captured JSON text. main() wraps it: reads the two file paths as
argv, prints one JSON row per merged hit. This is a candidate list, not a
verdict — every row still needs the judgment pass.
Pass two — judge every row. For each row, read the except in context:
is there a comment explaining why the silence is safe? Is the exception
type narrow and named, or a blind catch-all? Is the failure re-raised,
logged, or otherwise surfaced anywhere nearby? Reassign bucket per the
rules above, and rewrite summary / failure to say why — "documented
as an optional best-effort notification at line N" for a justified, "no
comment, no re-raise, no log — a real error goes missing" for a fix.
Two rows can point at the same except block (ruff sometimes reports the
try's line and the except's line separately for one silence) — judge
them together and give them the same verdict.
Write the findings log and render the summary — the default
deliverable. See
~/.agents/skills/all-audits/harness/AUDIT-RUN.md for the shared
write-and-deliver step (tmpdir resolution, findings.jsonl +
report.html, opening, and the final print). This audit
touches no code — fixing a swallowed error is a separate, opt-in step the
user asks for by name. This skill's own bucket names and metabar:
- Log — one JSONL line per merged hit.
bucket is fix / justified
/ unsure. category is the tool-code slug (see above). extra.codes
carries every ruff/bandit code that fired on that line.
- Summary — the verdict, the
N flagged · F fix · J justified · U unsure metabar, findings grouped by bucket then category with counts.
No per-hit cards. Call out the justified finds in a vt-callout — the
ones a naive tool-only read would have wrongly told the user to fix.
Verify against the fixture
~/.agents/skills/error-handling/fixtures/answer-key.md is the fixture's spec — the captured
ruff + bandit output, the parser's mechanical rows, and the pass-two verdict
for each. Running this skill over ~/.agents/skills/error-handling/fixtures/ must reproduce
it exactly.
1---2name: error-handling3description: Find swallowed errors — bare excepts, `except Exception: pass`, silent drops — and sort a justified silence from an unjustified one, enforcing the repo's fail-loud rule where a plain linter stops short.4---56Run ruff + bandit over the target, then judge every hit: is this silence7deliberate and correct, or is an error going missing with nobody the wiser?8A plain linter flags `except Exception: pass` the same way whether it's a9documented, narrow, best-effort suppression or a lazy catch-all hiding a real10bug — the judgment pass here is what tells them apart.1112**Two passes.** `audit.py` in this skill's directory is pass one —13mechanical: run ruff and bandit, parse their JSON output into findings rows.14It never judges justified vs unjustified; every row it emits starts `bucket:15fix` (a caught-and-dropped exception is a real smell by construction — same16reasoning `duplication`'s parser uses for a token clone). The judgment pass17below reads those rows and reassigns the real bucket.1819## Buckets & categories2021- **`fix`** — the silence is unjustified: no re-raise, no log, no comment22 explaining why swallowing is correct here. Make it fail loud — log it,23 re-raise it, or narrow the caught type.24- **`justified`** — the silence is deliberate and correct: a documented25 reason (a comment naming why this failure is expected and safe to ignore),26 a genuinely-optional cleanup, or `contextlib.suppress` with a narrow named27 exception and a comment. Leave it.28- **`unsure`** — flagged, and it isn't a clean fit for either bucket above:29 no comment, but the surrounding code makes the intent ambiguous rather than30 clearly careless.3132`category` is the tool code slugged into a small closed set: `bare-except`33(ruff `BLE001`), `try-except-pass` (ruff `SIM105` / bandit `B110`),34`try-except-continue` (bandit `B112`), `raise-without-from` (ruff `B904`),35`try-consider` (any ruff `TRY0xx`). When more than one code fires on the same36line, `bare-except` > `raise-without-from` > `try-except-continue` >37`try-except-pass` > `try-consider` picks the row's category; `extra.codes`38lists every code that fired, so pass two sees the full mechanical picture.3940## Run41421. **Scope tight.** Audit `$ARGUMENTS` if given; with no argument, scope43 defaults per `~/.agents/skills/all-audits/SKILL.md`'s Scope section. Skip44 vendored, generated, and dependency trees (`node_modules`, `dist`, `.venv`,45 `vendor`, build output, lockfiles) and any `.git/` or `worktrees/` tree.46472. **Pass one — run ruff and bandit over the SAME absolute scope, parse48 them.**49 ```sh50 scope="$(realpath "${ARGUMENTS:-.}")"51 uvx ruff check --select BLE,TRY,B904,SIM105 --output-format json "$scope" > /tmp/ruff-out.json52 uvx bandit -r "$scope" -f json -t B110,B112 -q \53 --exclude "$scope/node_modules,$scope/.venv,$scope/dist,$scope/vendor,$scope/.git,$scope/build,$scope/worktrees" \54 > /tmp/bandit-out.json55 python3 ~/.agents/skills/error-handling/audit.py /tmp/ruff-out.json /tmp/bandit-out.json56 ```57 Using the same absolute path for both tools matters: ruff's JSON always58 reports absolute `filename`s; bandit's mirrors whatever scope you gave it.59 Different scope forms mean different filename strings, and same-line hits60 from the two tools won't merge into one row.6162 `audit.py`'s `parse_findings(ruff_json, bandit_json) -> list[dict]` is the63 tested seam (`~/.agents/skills/error-handling/fixtures/` + `answer-key.md` back it,64 mirroring `~/.agents/skills/dead-code/fixtures/`) — pure, no subprocess inside it, fed both65 tools' captured JSON text. `main()` wraps it: reads the two file paths as66 argv, prints one JSON row per merged hit. This is a candidate list, not a67 verdict — every row still needs the judgment pass.68693. **Pass two — judge every row.** For each row, read the except in context:70 is there a comment explaining why the silence is safe? Is the exception71 type narrow and named, or a blind catch-all? Is the failure re-raised,72 logged, or otherwise surfaced anywhere nearby? Reassign `bucket` per the73 rules above, and rewrite `summary` / `failure` to say *why* — "documented74 as an optional best-effort notification at line N" for a `justified`, "no75 comment, no re-raise, no log — a real error goes missing" for a `fix`.76 Two rows can point at the same except block (ruff sometimes reports the77 `try`'s line and the `except`'s line separately for one silence) — judge78 them together and give them the same verdict.79804. **Write the findings log and render the summary — the default81 deliverable.** See82 `~/.agents/skills/all-audits/harness/AUDIT-RUN.md` for the shared83 write-and-deliver step (tmpdir resolution, `findings.jsonl` +84 `report.html`, opening, and the final print). This audit85 touches no code — fixing a swallowed error is a separate, opt-in step the86 user asks for by name. This skill's own bucket names and metabar:8788 - **Log** — one JSONL line per merged hit. `bucket` is `fix` / `justified`89 / `unsure`. `category` is the tool-code slug (see above). `extra.codes`90 carries every ruff/bandit code that fired on that line.91 - **Summary** — the verdict, the `N flagged · F fix · J justified · U92 unsure` metabar, findings grouped by bucket then category with counts.93 No per-hit cards. Call out the `justified` finds in a `vt-callout` — the94 ones a naive tool-only read would have wrongly told the user to fix.9596## Verify against the fixture9798`~/.agents/skills/error-handling/fixtures/answer-key.md` is the fixture's spec — the captured99ruff + bandit output, the parser's mechanical rows, and the pass-two verdict100for each. Running this skill over `~/.agents/skills/error-handling/fixtures/` must reproduce101it exactly.