scripts/scan.sh has two defects that let a stocktake report confident results from data it does not have. Both are in the current main (211-line scan.sh). Related to #2598 and #2801, which cover other traversal defects in the same file.
1. Trashed skills are counted as live
scan_dir_to_json() runs find -L "$dir" -name "SKILL.md" -type f with no exclusion for .trash/. On a machine where skills have been removed through the skills CLI, ~/.claude/skills/.trash/<timestamp>-<pid>-<rand>/<name>/SKILL.md still matches.
Observed on one machine: the scan reported 179 skills where 117 were live. 62 were trashed copies.
Three consequences. The inventory overstates the skill count by however many have been removed. Evaluation budget is spent writing verdicts on skills the user already deleted. And because several trashed copies share a name with a live skill, the "content overlap with other skills" checklist item flags skills as overlapping with their own tombstones.
It also corrupts the cache. Those 62 entries are written into results.json, so every subsequent Quick Scan diffs against them.
Suggested fix: add -not -path '*/.trash/*' to the find in scan_dir_to_json(), in both scan.sh and quick-diff.sh.
2. Unmeasured usage is reported as 0
count_obs() returns 0 when $OBSERVATIONS does not exist:
count_obs() {
local file="$1" cutoff="$2"
if [[ ! -f "$OBSERVATIONS" ]]; then
echo 0
return
~/.claude/observations.jsonl is not created by Claude Code by default, so on a stock install every skill's use_7d and use_30d are 0. The aggregated fast path defaults to 0 the same way.
SKILL.md then renders those as "7d use" and "30d use" columns in the Phase 1 inventory table, and the Phase 2 checklist lists "Usage frequency considered" as an item to be satisfied. Nothing anywhere states that the file may be absent or that the columns may be meaningless — the words "observations.jsonl" and "unmeasured" do not appear in SKILL.md at all.
The result is that a check which could not run is indistinguishable from a check that ran and found zero usage, and a reviewer can reasonably retire a skill on evidence that does not exist.
Suggested fix: emit null or the string unmeasured rather than 0 when the file is absent; render that distinctly in the inventory table; and add a line to SKILL.md stating that absent usage data is never evidence for a verdict.
Why these belong together
Both are the same failure: absent data presented as measured data. The first inflates a count, the second fabricates a zero. Either alone produces a stocktake that looks complete and is not.
scripts/scan.shhas two defects that let a stocktake report confident results from data it does not have. Both are in the currentmain(211-linescan.sh). Related to #2598 and #2801, which cover other traversal defects in the same file.1. Trashed skills are counted as live
scan_dir_to_json()runsfind -L "$dir" -name "SKILL.md" -type fwith no exclusion for.trash/. On a machine where skills have been removed through theskillsCLI,~/.claude/skills/.trash/<timestamp>-<pid>-<rand>/<name>/SKILL.mdstill matches.Observed on one machine: the scan reported 179 skills where 117 were live. 62 were trashed copies.
Three consequences. The inventory overstates the skill count by however many have been removed. Evaluation budget is spent writing verdicts on skills the user already deleted. And because several trashed copies share a name with a live skill, the "content overlap with other skills" checklist item flags skills as overlapping with their own tombstones.
It also corrupts the cache. Those 62 entries are written into
results.json, so every subsequent Quick Scan diffs against them.Suggested fix: add
-not -path '*/.trash/*'to thefindinscan_dir_to_json(), in bothscan.shandquick-diff.sh.2. Unmeasured usage is reported as 0
count_obs()returns0when$OBSERVATIONSdoes not exist:~/.claude/observations.jsonlis not created by Claude Code by default, so on a stock install every skill'suse_7danduse_30dare0. The aggregated fast path defaults to0the same way.SKILL.md then renders those as "7d use" and "30d use" columns in the Phase 1 inventory table, and the Phase 2 checklist lists "Usage frequency considered" as an item to be satisfied. Nothing anywhere states that the file may be absent or that the columns may be meaningless — the words "observations.jsonl" and "unmeasured" do not appear in SKILL.md at all.
The result is that a check which could not run is indistinguishable from a check that ran and found zero usage, and a reviewer can reasonably retire a skill on evidence that does not exist.
Suggested fix: emit
nullor the stringunmeasuredrather than0when the file is absent; render that distinctly in the inventory table; and add a line to SKILL.md stating that absent usage data is never evidence for a verdict.Why these belong together
Both are the same failure: absent data presented as measured data. The first inflates a count, the second fabricates a zero. Either alone produces a stocktake that looks complete and is not.