{"slug":"rot-canary","title":"rot-canary","summary":"Code-health scan — dead code, bug-prone logic, resource leaks, concurrency bugs, silent failures, input-boundary issues, doc rot. Triggers on: \"/rot-canary\", \"rot-canary\", \"code-health\" (legacy aliases: \"/rotcanary\", \"rotcanary\"). Auto-runs at session end on touched files (QUICK,","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-18T14:07:43.402543Z","repo":{"url":"https://github.com/TheColliery/CoalMine","stars":13,"forks":2,"license":"Apache-2.0","updatedAt":"2026-09-24T10:31:09Z"},"bodyHtml":"<hr>\n<h2>name: rot-canary\ndescription: &gt;-\nCode-health scan — dead code, bug-prone logic, resource leaks, concurrency bugs, silent failures, input-boundary issues, doc rot. Triggers on: \"/rot-canary\", \"rot-canary\", \"code-health\" (legacy aliases: \"/rotcanary\", \"rotcanary\"). Auto-runs at session end on touched files (QUICK, report only) via platform hooks — auto-wired by the Claude Code plugin, manual elsewhere. Run manually for fix mode. Reports; fixes on request via choice-gated menu.</h2>\n<h1>Rot-Canary</h1>\n<p><strong>Language:</strong> Generate EVERYTHING at runtime in the user's language — questions, answer options, menu labels, recommendations, report narrative. Detect from their messages; never default to English just because this file is English. English is allowed only for technical terms: commands, paths, code identifiers, severity labels (CRITICAL/HIGH/MEDIUM/LOW), and tier names (Light/Standard/Heavy).</p>\n<p><strong>Config reads — every config key, always the CASCADE, never the bare project file:</strong> <code>~/.claude/.coalmine.json</code> first, then the project config (own agent dir → other known agent dirs → legacy <code>&lt;gitroot&gt;/.coalmine.json</code>), project wins per key. A bare project read is ABSENT on a machine configured only globally, so it silently yields defaults.</p>\n<p>Scan code for rot. Report CONFIRMED findings. Fix on request.</p>\n<h2>Parameters</h2>\n<ul>\n<li><strong>SCOPE:</strong> touched files (default) | diff | named files | whole repo. Touched-files scan is hybrid-capped: all if ≤ <code>autoScanFileCap</code>, else the <code>autoScanFileCapSlice</code> most-recently-modified files (warn the user). A touched file matching <code>scanExcludePaths</code> (lab/throwaway tooling only — never shipped/tracked source) is dropped before the cap; the nudge notes the skip count.</li>\n<li><strong>DISCLOSE EVERY SCOPE CUT, always — a suppressed finding must never look like an absent one.</strong> Whenever the scope you actually scanned is narrower than the scope you were asked for, say so IN THE REPORT, with the COUNT and the KNOB that did the cutting: files dropped by <code>scanExcludePaths</code>, files left unscanned by the <code>autoScanFileCap</code> slice, file types outside <code>watchedExtensions</code>. State it even when the scan found nothing — that is exactly the case where the omission is invisible, because \"scanned, clean\" and \"never scanned\" read identically to a user. <strong>If EVERY file in scope was cut, that is not a clean report: say plainly that no scan ran, and name what cut it.</strong> <strong><code>scanEverything: true</code> bypasses every scan-scope cut at once</strong> (<code>scanExcludePaths</code> ignored, <code>autoScanFileCap</code> not applied) — offer it when a user asks why files were skipped. It does NOT re-enable a disabled canary, and it does NOT reach the recording-side cuts (<code>watchedExtensions</code>, tmpdir), which decide what is recorded before any scan-time key is read, nor the tripwire's <code>tripwireMaxFileSizeKb</code> cap (an over-cap file IS recorded and IS scanned — only its edit-time pre-flag is skipped). <strong>So report it as an unfiltered SCAN, never as \"everything\"</strong> — an incompletely-widened scan that reads as fully widened is the same trust defect as a suppressed scan reading as a clean one, with the sign reversed. <strong>Read it through the merged config, never the project file alone — for TWO independent reasons, and the second is the one CWK-057 left out:</strong> (1) the READ PATH — a bare project file is ABSENT on a machine configured only globally, so an agent reading it sees nothing and silently uses defaults; (2) the CLAMP — a project-level <code>true</code> is CLAMPED to <code>false</code> unless the global layer also says <code>true</code> (<code>hooks-safety.md</code> §9 — a cloned repo must not be able to force a full scan on you), so a raw project-file read would report a scope that is not what the hook actually ran. (The Stop-hook auto-scan path already emits its own equivalents — <code>capNotice</code>, <code>scanExcludeNotice</code>, and the all-excluded quiet note — in all five languages; this rail is the MANUAL path's counterpart, which has no hook to speak for it.)</li>\n<li><strong>FILE TYPES:</strong> code only by default, matching <code>watchedExtensions</code> (source files — never docs/prose/config-prose, CoalLedger's axis). Name non-code files explicitly to include them.</li>\n<li><strong>DEPTH:</strong> QUICK (default) | DEEP</li>\n</ul>\n<h2>Categories</h2>\n<ol>\n<li><strong>Bug-risk</strong> — null deref, wrong operator, off-by-one, missing return</li>\n<li><strong>Dead / unreachable</strong> — zero-ref symbols, code after return/throw, always-true guards</li>\n<li><strong>Disconnected</strong> — exists but never wired to entry point, half-done refactor</li>\n<li><strong>Duplication</strong> — copy-paste diverged, two sources of truth for one constant</li>\n<li><strong>Resource leak</strong> — undisposed handle/stream/COM, subscription never removed</li>\n<li><strong>Async</strong> — unawaited task, <code>.Result</code>/<code>.Wait()</code> deadlock, blocking on UI thread</li>\n<li><strong>Silent failure</strong> — empty catch, success on partial completion, ignored return code</li>\n<li><strong>Input security</strong> — unvalidated input, injection, path traversal, secret in code/log</li>\n<li><strong>Performance</strong> — O(n²) in hot path, N+1, unbounded growth, work on UI thread</li>\n<li><strong>Doc rot</strong> — comment contradicts code, stale TODO, wrong param in docstring</li>\n</ol>\n<h2>Discipline</h2>\n<ul>\n<li>Report only CONFIRMED. Unverifiable → separate \"SUSPECTED\" list.</li>\n<li>Cite evidence (file:line, call-site count, the absent catch).</li>\n<li>\"Dead\" = <strong>zero-reference reachability</strong> (the static heuristic): zero references across ALL entry routes — reflection, DI, events, public API, tests — not a single-file grep.</li>\n</ul>\n<h2>Fix mode (choice-gated)</h2>\n<p><strong>Before deciding fix mode:</strong> read <code>~/.claude/.coalmine.json</code> then the project config (own agent dir → other known agent dirs → legacy <code>&lt;gitroot&gt;/.coalmine.json</code>; project wins per key); neither present → <code>autoFixMode</code> = <code>interactive</code>.</p>\n<p><strong>Standing consent:</strong> honor <code>.coalmine.json</code> <code>autoFixMode</code> as the pre-chosen option (the config IS the chosen option) — <code>off</code> = report only, no menu · <code>safe</code> = apply safe/reversible fixes automatically (still checkpoint → build/test → revert if red) · <code>interactive</code> (default) = present the menu below.</p>\n<p>After any scan report in an interactive session — manual run OR hook-nudged auto-scan — you <strong>MUST</strong> present this menu via <code>ask_question</code> (skip only when findings are zero, no user is present, or <code>autoFixMode</code> pre-decided above):</p>\n<ul>\n<li><strong>Apply safe fixes:</strong> mechanical, fully reversible edits only (dead imports, commented-out blocks, formatting). Each fix: checkpoint (git stash/commit in a git repo; else copy the file aside — never assume git exists) → apply → build + tests → auto-revert if newly red.</li>\n<li><strong>Let me pick:</strong> list findings; user selects.</li>\n<li><strong>Report only:</strong> exit unchanged.</li>\n</ul>\n<p>NEVER auto-fix: live/reachable path · logic change · \"API looks wrong\" (ground via source-grounding first) · framework-wired code that only <em>looks</em> dead · SUSPECTED findings.</p>\n<h2>Grants &amp; denials (CLASSIFY-BLOCK)</h2>\n<table>\n<thead>\n<tr>\n<th>class</th>\n<th>step it powers</th>\n<th>grant</th>\n<th>on denial</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>read</td>\n<td>scan the touched/named files for the categories above</td>\n<td><code>Read</code>·<code>Grep</code>·<code>Glob</code>·<code>Bash</code> (read-only)</td>\n<td>refuse that file, name it in the report — never a clean bill</td>\n</tr>\n<tr>\n<td>write</td>\n<td>Fix mode's safe/interactive apply, incl. checkpoint → build+tests → auto-revert if newly red</td>\n<td><code>Edit</code>·<code>Bash</code> (checkpoint/build/revert need exec, not just file-write)</td>\n<td>report the fix as NOT applied AND the checkpoint/revert as NOT available, never claim done — this skill runs unattended on the Stop hook under <code>autoFixMode: safe</code>, with no interactive user to notice a denial, so the report line is the only signal and it says so</td>\n</tr>\n</tbody>\n</table>\n<p>A denial reaches the WORKER as a visible message and propagates no further — never to a\ncaller, never as a catchable condition. Every row above states a grant or an explicit death;\na step that dies says so in the output, never as a false \"done\"/\"skipped\"/\"clean\".</p>\n<ul>\n<li><strong>read</strong> denied → refuse before scanning; never a false clean bill.</li>\n<li><strong>write</strong> denied → report the change as NOT applied — never claim done.</li>\n<li><strong>network</strong> denied/unfetchable → <code>⚠️ unverified: check [source]</code>.</li>\n<li><strong>spawn</strong> denied → degrade per Escalation's own capability-lever fallback (never fake\nparallelism) and say the fan-out did not happen — already discharged there; a row above\nis only for a spawn this skill does OUTSIDE tier escalation.</li>\n</ul>\n<h2>Output</h2>\n<p>| # | path:line | category | severity | finding | evidence | fix |</p>\n<p>Then: SUSPECTED list · coverage gaps · counts + top 3 to fix.</p>\n<p>Severity: CRITICAL (data loss/security/crash on normal path) · HIGH (real bug/leak on reachable path) · MEDIUM (dead/dup/unwired) · LOW (style/doc rot)</p>\n<p><strong>Reporting:</strong> call <code>ReportFindings</code> when callable — <code>file</code>/<code>line</code> MUST be the defect site, never the enclosing function; an unresolvable line reports your best guess, named imprecise in the wrap-up — <strong>never dropped, never faked.</strong> Severity prefixed in <code>summary</code> (e.g. <code>[HIGH] …</code>), ranked most-severe first, SUSPECTED as <code>verdict: PLAUSIBLE</code>; chat then carries only the wrap-up line (counts · coverage gaps · overflow past 32 · any imprecise-line findings) + the fix menu, never a restatement of findings. Not callable → the table above, unchanged. An Apply-fixes click = consent to the safe-fix class only — gated the same as this skill's own fix-mode (Hook Context needs an interactive session, per the Hook Context rule below) — composing with (never bypassing) the fix-mode discipline. <strong>After any fix round, re-report the same findings with <code>outcome: fixed</code>/<code>skipped</code>/<code>no_change_needed</code> — skipping this leaves the round UNFINISHED.</strong></p>\n<h2>Cadence</h2>\n<p>Stop hook → auto QUICK on the session's touched files (report only), hybrid-capped per <code>.coalmine.json</code> (see Parameters). Manual whole-repo DEEP sweep when needed. Auto-wiring is platform-dependent — read <code>references/cadence.md</code> before claiming auto-scan works on the current platform.</p>\n<h2>Tooling</h2>\n<p>Per-stack build/dead-code/lint commands: read <code>references/tooling.md</code> when selecting scan tools.</p>\n<h2>Escalation — Scope &amp; Model Quality</h2>\n<p>Tiers are <strong>capability targets</strong>, not platform commands — resolve each to your host's nearest lever. No lever for one? <strong>Degrade gracefully — never fake parallelism you can't do</strong>; escalate via model tier + reasoning depth instead.</p>\n<table>\n<thead>\n<tr>\n<th>Level</th>\n<th>Intent</th>\n<th>Capability target</th>\n<th>Cost</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Light</strong></td>\n<td>Fast scan, minimal coverage</td>\n<td>Cheapest model · single agent, no sub-agents.</td>\n<td>Low</td>\n</tr>\n<tr>\n<td><strong>Standard</strong></td>\n<td>Balanced scan, module-level coverage</td>\n<td>Balanced model · raised reasoning · sub-agents per category <strong>only if your platform runs concurrent workers</strong> (else single-agent).</td>\n<td>Balanced</td>\n</tr>\n<tr>\n<td><strong>Heavy</strong></td>\n<td>Full scan, maximum coverage</td>\n<td>Most capable model + largest context · deepest reasoning · max sub-agent fan-out <strong>if supported</strong> · adversarial cross-check where available.</td>\n<td>High</td>\n</tr>\n</tbody>\n</table>\n<p>Per-platform Heavy levers + Heavy-run durability: read <code>references/escalation.md</code> before a Heavy run. No concurrent fan-out on your host → escalate by model + reasoning only.</p>\n<p><strong>Agent Context (interactive):</strong> score the tier rubric, then call <code>ask_question</code> once with the 3 tiers — the pick marked <code>✓</code>, score shown, labels localized — and wait for the choice before starting. <code>ask_question</code> = your platform's question tool: Claude Code <code>AskUserQuestion</code> · Cline <code>ask_question</code> · Copilot <code>askQuestions</code> · Gemini CLI <code>ask_user</code> (business-tier product; individual tiers ended 2026-06-18 → Antigravity CLI) · Codex <code>request_user_input</code> · Cursor/Devin Desktop (ex-Windsurf)/Antigravity built-in prompts; none → numbered text menu.</p>\n<p><strong>Tier rubric (deterministic):</strong> +1 each — ① &gt;20 files or whole-repo/cross-module reach ② &gt;2 of this skill's categories relevant ③ release/security/pre-ship context ④ findings will drive code changes. <strong>0–1 Light · 2–3 Standard · 4 Heavy.</strong> <strong>Freshness cap:</strong> scope already audited ≥Standard this session → cap at Light (re-auditing fresh ground wastes tokens; scope to what changed). <strong>Default tier:</strong> honor <code>.coalmine.json</code> <code>defaultTier</code> unless the user requests a tier for that run — an explicit request overrides everything.</p>\n<p><strong>Hook Context (auto-triggered):</strong> auto-Light, no tier question, no sub-agents — report first. Interactive session (a user is present) → follow this skill's own Fix mode section, if it defines one, for what to offer after the report; non-interactive → report-only. Where a Fix mode section exists, never fix without a chosen option.</p>\n<p><strong>Entanglement:</strong> after the report, if confirmed findings fall in another canary's domain, offer it once via <code>ask_question</code> (one line, max one offer): perf/N+1 → scale-canary · contract/serialization/config → drift-canary · failure-path/retry → resilience-audit · logging/metrics → telemetry-canary · coupling/DI → testability-canary · dependency/CVE → supply-chain-audit · unverified version-sensitive claim → source-grounding · missing/stale rule → gold-standard.</p>\n<p><strong>Self error-report:</strong> if this skill misbehaves (contradictory instruction, broken procedure, wrong finding class), OFFER to file it at <a href=\"https://github.com/HetCreep/CoalMine/issues/new/choose\">https://github.com/HetCreep/CoalMine/issues/new/choose</a> with a user-reviewed summary — never auto-submit, never include unapproved code or paths.</p>\n","files":[{"path":"references/cadence.md","sizeBytes":3609,"isText":true},{"path":"references/escalation.md","sizeBytes":1429,"isText":true},{"path":"references/tooling.md","sizeBytes":724,"isText":true},{"path":"SKILL.md","sizeBytes":13897,"isText":true},{"path":"skill-meta.json","sizeBytes":153,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-27T19:47:09.483081Z","sha256":"5D87906E9B5D83DA1B88302948BD1FF475CBBE4417D5DB9A0B1CB4C7F82C8AF7","sizeBytes":10066},"review":null,"source":{"repositoryUrl":"https://github.com/TheColliery/CoalMine","path":"plugin/skills/rot-canary","license":"Apache-2.0","commit":"4e844291d1e72f7c370421e96d5573757c0cae5f","subtreeSha":"EE1CB26457FBAEE8F11F1E33286CBF3D693FA3111D072C40315BC451B2F89FCE","lastSyncedAt":"2026-09-27T19:46:39.429499Z"},"reviewedAt":"2026-09-27T19:47:40.886148Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/TheColliery/CoalMine/tree/main/plugin/skills/rot-canary"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install hetcreep-coalmine@llmmart"},{"target":"git","command":"git clone https://github.com/TheColliery/CoalMine.git"}]}