Smell Scan
Turn a passive Clean Code reading into a reactive analysis of real code. Given a target — a file, a
directory, or a specific function/type — detect concrete code smells from the refactoring.guru taxonomy,
persist them as a source-of-truth (SoT) file per domain, report each finding with its location, confidence,
and mapped techniques, then offer to apply a fix via the refactor skill.
Detection always fans out through a Workflow that pipelines each domain through its own 5-detector
batch — one domain or many, the mechanism is the same, so there is only one synthesis path to keep
correct. Detection and reporting only — this skill never edits code.
Step 1 — Resolve the target
Determine what to scan from the user's request:
- An explicit path (
src/order-service.ts), a directory, or a named function/type. - If the request is "this" / "this code" with no path, use the file or symbol currently in focus in the conversation. If genuinely ambiguous, ask one short clarifying question before scanning.
Step 2 — Partition into domains
A domain is a top-level module folder under a conventional root: src/, packages/, plugins/, or
the repo root itself when none of those roots is present. Map the resolved target UP to the domain(s) that
contain it:
- A single file or a nested directory maps to the one top-level folder that contains it.
- If the target itself IS one of those roots (e.g. the user asks to scan all of
plugins/), every immediate child directory of that root is its own domain.
domainCount is the number of distinct top-level domains the target resolves to. The domain identifier
used everywhere downstream (SoT filename, domain field, path field on findings) is the repo-relative
path (e.g. src/orders, plugins/refactoring-guru). Absolute paths are what gets passed to the Workflow
in Step 3; the script derives the repo-relative form itself from the repoRoot argument, so no manual
conversion happens here.
If domainCount is 0 (the target does not resolve to anything scannable), tell the user there is nothing
to scan and stop here.
Step 3 — Dispatch the Workflow
First capture two values the script needs but cannot derive itself — one scanned_at timestamp
(Date.now()/new Date() would break Workflow resume) and the absolute repoRoot (used to normalize
paths and to build absolute write paths):
date -u +%Y-%m-%dT%H:%M:%SZ
git rev-parse --show-toplevel
Then invoke, regardless of domainCount — one domain and many both flow through the same pipeline, so
there is no separate manual synthesis path to keep in sync by hand:
Workflow({ scriptPath: "${CLAUDE_PLUGIN_ROOT}/skills/smell-scan/scripts/workflow.js", args: { domains: ["<absolute path to domain 1>", "<absolute path to domain 2>", ...], repoRoot: "<absolute repo root>", scannedAt: "<captured ISO timestamp>" } })
The Workflow pipelines each domain through its own 5-detector batch (bloaters, oo-abusers,
change-preventers, dispensables, couplers) and synthesizes its findings in plain JS (see the
script's synthesizeDomain): merging the 5 detectors' results, normalizing file to a repo-relative
path (via repoRoot), deriving technique from the head of techniques, computing severity from
confidence bands, and flagging cross_cutting for Shotgun Surgery, Inappropriate Intimacy, and
Divergent Change. It then assigns reference codes globally across every domain (stable-sorted by
confidence descending, one counter per category prefix — B/OO/CP/D/C — never restarting per
domain) and serializes one source-of-truth file per domain.
It returns { domains: [{ domain, total, findings }], total, files: [{ path, content }] } with no
filesystem writes — the files array carries each domain's SoT already serialized, with an absolute
path (joined under repoRoot) ready to persist verbatim in Step 4.
Step 4 — Persist the returned files
The Workflow has already assigned codes and serialized each domain's source-of-truth file, and each
files entry's path is already absolute. This step has no derivation left — just write what the
script returned:
mkdir -p "<repoRoot>/.claude/refactoring-guru/findings"
Then, for each entry of the returned files array, Write(file_path=<entry.path>, content=<entry.content>)
verbatim. Each content is a fully-formed SoT document; do not reshape, re-sort, or re-slug it, and do
not rewrite the absolute path. A clean domain (zero findings) still returns an entry — write it too;
its SoT records the scan with an empty findings array. The content shape, for reference:
{
"domain": "src/orders",
"scanned_at": "2026-06-30T14:00:00Z",
"total": 2,
"findings": [
{
"code": "B1",
"smell": "Long Method",
"category": "Bloaters",
"path": "src/orders/order-service.ts",
"line_range": [45, 120],
"evidence": "75-line function performs validation, transformation and persistence",
"confidence": 92,
"severity": "high",
"technique": "Extract Method",
"resolution_plan": "Extract validation (45-60), transformation (61-95), persistence (96-120) into three named functions; the body becomes three calls.",
"cross_cutting": false
}
]
}
Step 5 — Present the prioritized report
Render the findings ordered by severity (critical → high → medium → low). Each finding's reference code
(B1, B2, OO1, CP1, D1, C1, …) is a per-scan handle the user can pass straight to refactor.
For each finding, show:
### <code> · <smell> — <severity> (confidence <n>)
- File: <path>:<line_start>-<line_end>
- Evidence: <evidence>
- Technique: <technique>
When domainCount > 1, also show the domain each finding belongs to, grouped or annotated so the user can
tell which SoT file it lives in. Group by severity band; within a band, keep confidence ordering. Lead the
report with a one-line summary: total findings and the count per severity. If total is 0 across every
domain, say the target is clean of high-confidence smells — do not pad with low-confidence noise.
Reference codes are assigned per scan, sequentially within each category prefix, globally across every
domain scanned. They are stable only within this scan; re-scanning may renumber. The smell types and
their detection criteria live in ${CLAUDE_PLUGIN_ROOT}/references/smell-catalog.md.
Step 6 — Offer to refactor
After the report, offer to apply a fix:
- The user can pass a reference code (
refactor B1) or name a technique and location explicitly. - Invoking the
refactorskill resolves the code unambiguously — even across a multi-domain scan, since codes are globally unique — and carries the finding's technique, file, location, and smell context into a safe test → refactor → test → commit cycle. Recommend tackling findings highest-severity first, and one technique at a time.
Do not start refactoring from this skill. This skill detects and reports; refactor applies.
References
Load on demand, only when needed:
${CLAUDE_PLUGIN_ROOT}/references/smell-catalog.md— the 26 smells: reference code, detection criteria, problem, and mapped techniques. Consult to explain a finding or justify a confidence call.${CLAUDE_PLUGIN_ROOT}/references/refactoring-techniques.md— the 67 techniques: when to apply and mechanics. Consult to explain why a technique maps to a smell.${CLAUDE_PLUGIN_ROOT}/references/workflow.md— the safe test → refactor → test → commit cycle that any fix should follow.
Scope notes
- The taxonomy includes OOP-specific smells (marked in the catalog). For non-OOP targets, detectors do not invent class/inheritance smells — those surface only when real type-hierarchy code is present.
- Findings are evidence-based: every one cites a file and line range with a concrete observation. Silence (zero findings) is a valid, correct result for clean code.
- SoT files under
.claude/refactoring-guru/findings/are transient analysis artifacts, gitignored — they are not meant to be committed.