@chimix/agentscan 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +475 -0
- package/package.json +56 -0
- package/src/analyze.ts +82 -0
- package/src/checks/budgets.ts +189 -0
- package/src/checks/index.ts +738 -0
- package/src/checks/make.ts +37 -0
- package/src/cli.ts +230 -0
- package/src/commands/check.ts +64 -0
- package/src/commands/explain.ts +64 -0
- package/src/commands/init.ts +42 -0
- package/src/commands/rules.ts +36 -0
- package/src/config/load.ts +26 -0
- package/src/config/schema.ts +63 -0
- package/src/discover/index.ts +888 -0
- package/src/facts/extract.ts +273 -0
- package/src/facts/semver.ts +41 -0
- package/src/facts/types.ts +113 -0
- package/src/report/clipboard.ts +90 -0
- package/src/report/exit-code.ts +36 -0
- package/src/report/json.ts +36 -0
- package/src/report/prompt.ts +102 -0
- package/src/report/safe.ts +0 -0
- package/src/report/score.ts +47 -0
- package/src/report/sort.ts +26 -0
- package/src/report/text.ts +142 -0
- package/src/version.ts +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sima Alexandru
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
<h1 align="center">agentscan</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<em>Your agent config says the guard is on. The script is gone. Nothing told you.</em>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="https://img.shields.io/badge/checks-23-111111?style=flat-square" alt="23 checks">
|
|
9
|
+
<img src="https://img.shields.io/badge/tests-183%20passing-111111?style=flat-square" alt="183 tests">
|
|
10
|
+
<img src="https://img.shields.io/badge/network-none-111111?style=flat-square" alt="No network">
|
|
11
|
+
<img src="https://img.shields.io/badge/writes-none-111111?style=flat-square" alt="Writes nothing">
|
|
12
|
+
<img src="https://img.shields.io/badge/runtime-bun-111111?style=flat-square" alt="Bun">
|
|
13
|
+
<img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<strong>23 checks · 3.4k lines · 0 network calls · every check sourced to a published spec line</strong><br>
|
|
18
|
+
<sub>Alpha. An earlier build reported 37 findings across 17 real projects of which <strong>25 were false</strong> — two checks had been written from what real projects looked like instead of from the spec. Both were deleted, and every check that survived is recorded in <a href="docs/spec/">docs/spec/</a> with the URL it came from and the date it was read. That story is the reason this tool exists in its current shape.</sub>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
Linters read the code your agent writes. This reads **the agent itself** — skills, `skills-lock.json`, hooks, MCP servers, agent definitions, policy files.
|
|
24
|
+
|
|
25
|
+
## The failure it exists for
|
|
26
|
+
|
|
27
|
+
You registered a `PreToolUse` hook to stop destructive shell commands:
|
|
28
|
+
|
|
29
|
+
```jsonc
|
|
30
|
+
// .claude/settings.json
|
|
31
|
+
{ "hooks": { "PreToolUse": [{ "hooks": [
|
|
32
|
+
{ "type": "command", "command": ".claude/hooks/guard-destructive-bash.js" }
|
|
33
|
+
] }] } }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Someone deleted the script six weeks ago.
|
|
37
|
+
|
|
38
|
+
```console
|
|
39
|
+
$ ls .claude/hooks/guard-destructive-bash.js
|
|
40
|
+
ls: cannot access '.claude/hooks/guard-destructive-bash.js': No such file or directory
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Your agent starts normally. No warning, no error, no log line. The guard you
|
|
44
|
+
think you have has silently not existed for six weeks.
|
|
45
|
+
|
|
46
|
+
```console
|
|
47
|
+
$ agentscan check
|
|
48
|
+
|
|
49
|
+
WARN hook:PreToolUse:.claude/hooks/guard-destructive-bash.js
|
|
50
|
+
rule:hook.missing-script
|
|
51
|
+
PreToolUse hook points at a script that does not exist
|
|
52
|
+
evidence: hook PreToolUse @ …/.claude/settings.json
|
|
53
|
+
|
|
54
|
+
Summary: 6 warn · 4 info hidden (--verbose) · score 40/100
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
That is the whole category: **a config file asserting something that is not true
|
|
58
|
+
of the filesystem**, where nothing else in your stack will ever tell you.
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
No AI, no network, no heuristics. Read the config, read the disk, compare:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
1. Discover .claude/ .agents/ .mcp.json AGENTS.md skills-lock.json
|
|
66
|
+
2. Extract immutable facts — never re-read during checking
|
|
67
|
+
3. Check 23 checks, each against one published spec line
|
|
68
|
+
4. Report text · --json · --output prompt (handoff for a fixing agent)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Same tree in, same findings out, every time. It never writes to the tree it
|
|
72
|
+
scans and never opens a socket.
|
|
73
|
+
|
|
74
|
+
## What it will not do
|
|
75
|
+
|
|
76
|
+
It will not compare a skill's frontmatter `name` to its directory, and it will
|
|
77
|
+
not validate model ids or MCP tool names. All three need a hardcoded list of
|
|
78
|
+
valid values, with everything absent from the list reported as broken. That is
|
|
79
|
+
exactly how the 25 false findings happened: a hook-event list with 9 names when
|
|
80
|
+
the spec has 31, calling working hooks dead at severity `error`.
|
|
81
|
+
|
|
82
|
+
A check here has to point at a spec line. If it cannot, it does not ship.
|
|
83
|
+
|
|
84
|
+
## Try it in 30 seconds
|
|
85
|
+
|
|
86
|
+
`agentscan` is **not on npm yet**, so `bunx agentscan` will 404. Clone and run it
|
|
87
|
+
against any project — it reads that project, writes nothing, and never leaves
|
|
88
|
+
your machine:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
git clone --depth=1 https://github.com/SimaAlexandru99/agentscan
|
|
92
|
+
cd agentscan && bun install
|
|
93
|
+
|
|
94
|
+
bun run src/cli.ts check ~/path/to/your-project
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Point it at a project that actually has agent config — a `.claude/` directory,
|
|
98
|
+
an `.mcp.json`, an `AGENTS.md`. On a project with none of those it will
|
|
99
|
+
correctly find nothing and say so.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# what it could possibly report, before you run it
|
|
103
|
+
bun run src/cli.ts rules
|
|
104
|
+
|
|
105
|
+
# the interesting bits it hides by default
|
|
106
|
+
bun run src/cli.ts check ~/your-project --verbose
|
|
107
|
+
|
|
108
|
+
# why one specific finding fired
|
|
109
|
+
bun run src/cli.ts explain hook.missing-script:hook:PreToolUse:./x.js ~/your-project
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Requires **Bun** 1.1+. There is no Node build: the CLI uses `import.meta.dir`
|
|
113
|
+
and `Bun.which` directly. `check` never opens a socket and never writes to the
|
|
114
|
+
tree it scans.
|
|
115
|
+
|
|
116
|
+
### In CI
|
|
117
|
+
|
|
118
|
+
The Action runs from its own checkout, so it works before the npm release:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
- uses: SimaAlexandru99/agentscan@v0
|
|
122
|
+
with:
|
|
123
|
+
fail-on: error
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### After the npm release
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
bun add -d agentscan
|
|
130
|
+
bunx agentscan check
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Usage
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
bunx agentscan check # text report (cwd)
|
|
137
|
+
bunx agentscan check ./my-app # explicit root
|
|
138
|
+
bunx agentscan check --json # machine-readable
|
|
139
|
+
bunx agentscan check --quiet # summary only
|
|
140
|
+
bunx agentscan check --verbose # include KEEP and info-severity findings
|
|
141
|
+
bunx agentscan check --fail-on warning
|
|
142
|
+
bunx agentscan check --global # also scan global skill dirs
|
|
143
|
+
|
|
144
|
+
bunx agentscan explain <findingId> # detail one finding
|
|
145
|
+
bunx agentscan rules # every check + rule id that can fire
|
|
146
|
+
bunx agentscan init # write .agentscanrc.json
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Flags for `check`:
|
|
150
|
+
|
|
151
|
+
| Flag | Meaning |
|
|
152
|
+
|------|---------|
|
|
153
|
+
| `--json` | JSON report (alias for `--output json`) |
|
|
154
|
+
| `--output <format>` | `human` (default) · `json` · `prompt` |
|
|
155
|
+
| `--copy` | Also copy the report to the system clipboard |
|
|
156
|
+
| `--quiet` | Summary line only |
|
|
157
|
+
| `--verbose` | Show KEEP + info-severity findings |
|
|
158
|
+
| `--fail-on <level>` | `never` (default) · `warning` · `error` |
|
|
159
|
+
| `--fail-under <0-100>` | Fail when the score drops below this floor |
|
|
160
|
+
| `--global` | Also scan `~/.claude/skills` and `~/.codex/skills` (see below) |
|
|
161
|
+
| `--config <path>` | Config file path |
|
|
162
|
+
|
|
163
|
+
**v1 does not write the tree** — no `apply`, no skill delete/install. Findings may *suggest* shell commands; you run them yourself.
|
|
164
|
+
|
|
165
|
+
## Sample output
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
agentscan v0.1.0 — touchagency
|
|
169
|
+
|
|
170
|
+
Stack: 46 deps · 54 skills · 1 mcp · 2 agents · packageManager=bun
|
|
171
|
+
|
|
172
|
+
WARN hook:PreToolUse:.claude/hooks/guard-destructive-bash.js
|
|
173
|
+
rule:hook.missing-script
|
|
174
|
+
PreToolUse hook points at a script that does not exist: .claude/hooks/guard-destructive-bash.js
|
|
175
|
+
evidence: hook PreToolUse @ …/.claude/settings.json · script .claude/hooks/guard-destructive-bash.js
|
|
176
|
+
|
|
177
|
+
WARN hook:PreToolUse:.claude/hooks/protect-artists-json.js
|
|
178
|
+
rule:hook.missing-script
|
|
179
|
+
PreToolUse hook points at a script that does not exist: .claude/hooks/protect-artists-json.js
|
|
180
|
+
evidence: hook PreToolUse @ …/.claude/settings.json · script .claude/hooks/protect-artists-json.js
|
|
181
|
+
|
|
182
|
+
Summary: 6 warn · 4 info hidden (--verbose) · score 40/100
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The `Stack:` line is orientation only — which project, how big. The summary lists
|
|
186
|
+
only actions that actually occurred; a clean project prints `Summary: no findings`.
|
|
187
|
+
|
|
188
|
+
JSON shape (abridged):
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"version": "0.1.0",
|
|
193
|
+
"root": "/path/to/project",
|
|
194
|
+
"factsSummary": {
|
|
195
|
+
"packageManager": "bun",
|
|
196
|
+
"depCount": 1,
|
|
197
|
+
"skillCount": 1,
|
|
198
|
+
"globalSkillCount": 0
|
|
199
|
+
},
|
|
200
|
+
"findings": [
|
|
201
|
+
{
|
|
202
|
+
"id": "hook.missing-script:hook:PreToolUse:.claude/hooks/protect-env.js",
|
|
203
|
+
"ruleId": "hook.missing-script",
|
|
204
|
+
"action": "warn",
|
|
205
|
+
"severity": "error",
|
|
206
|
+
"subject": "hook:PreToolUse:.claude/hooks/protect-env.js",
|
|
207
|
+
"message": "PreToolUse hook points at a script that does not exist: .claude/hooks/protect-env.js",
|
|
208
|
+
"reason": "The hook is registered but its script is missing, so it never runs.",
|
|
209
|
+
"evidence": [{ "kind": "script", "value": ".claude/hooks/protect-env.js" }],
|
|
210
|
+
"suggest": "Restore the script or remove the hook"
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Same tree → same sorted findings, with stable unique `id`s.
|
|
217
|
+
|
|
218
|
+
### `--global`
|
|
219
|
+
|
|
220
|
+
Adds `~/.claude/skills` and `~/.codex/skills` to the structural checks: a
|
|
221
|
+
`SKILL.md` that is malformed is malformed wherever it lives, and those findings
|
|
222
|
+
carry a `source: global` evidence entry so you can tell them apart.
|
|
223
|
+
|
|
224
|
+
Lockfile checks stay project-scoped — a project lockfile cannot pin a skill that
|
|
225
|
+
lives in your home directory, so reporting one as "not in the lockfile" could
|
|
226
|
+
only ever be wrong.
|
|
227
|
+
|
|
228
|
+
`includeGlobal: true` in `.agentscanrc.json` does the same thing without the
|
|
229
|
+
flag.
|
|
230
|
+
|
|
231
|
+
## Score
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
score = max(0, 100 - 10 × errors - 3 × warnings)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
That is the whole formula. Count two lines of the report and you can recompute
|
|
238
|
+
it. The comparable tools both score 0-100 and neither states its formula in
|
|
239
|
+
prose: shadscan's category weights are readable in its MIT source but not
|
|
240
|
+
documented, and react-doctor computes the score by POSTing your diagnostics to
|
|
241
|
+
`react.doctor/api/score`, so it cannot be recomputed offline at all. A number
|
|
242
|
+
whose derivation cannot be inspected is the false precision a score is supposed
|
|
243
|
+
to replace.
|
|
244
|
+
|
|
245
|
+
**Deduction, not coverage.** The obvious model — checks passed over checks run —
|
|
246
|
+
was measured against 17 real projects and rejected. Every one scored between
|
|
247
|
+
97.7% and 100%, so it discriminated nothing, and it *inverted* severity: the
|
|
248
|
+
denominator grows with the size of your config while the defects do not, so a
|
|
249
|
+
project with 85 skills and one warning outscored a project with 10 skills and
|
|
250
|
+
the same one warning. Deduction has no denominator. Measured on the same 17
|
|
251
|
+
projects it spans 40 to 100, and the 85-skill project scores 97 while a
|
|
252
|
+
54-skill one with six broken hooks scores 40.
|
|
253
|
+
|
|
254
|
+
**Info findings cost nothing.** They are budgets and hygiene notes, and they do
|
|
255
|
+
grow with project size — charging for them would reintroduce the inversion.
|
|
256
|
+
|
|
257
|
+
The score is a summary, not a verdict. `--fail-on error` is still the sharper
|
|
258
|
+
CI gate: it says which *kind* of problem fails the build, where `--fail-under`
|
|
259
|
+
only says how many points of unspecified trouble is too much.
|
|
260
|
+
|
|
261
|
+
## Exit codes
|
|
262
|
+
|
|
263
|
+
| Code | When |
|
|
264
|
+
|------|------|
|
|
265
|
+
| `0` | OK, or findings below `--fail-on` threshold (default `never` always `0` for findings) |
|
|
266
|
+
| `1` | Findings at/above `--fail-on`, or score below `--fail-under` |
|
|
267
|
+
| `2` | Usage / config / load error |
|
|
268
|
+
|
|
269
|
+
## CI
|
|
270
|
+
|
|
271
|
+
```yaml
|
|
272
|
+
- uses: SimaAlexandru99/agentscan@v0
|
|
273
|
+
with:
|
|
274
|
+
fail-on: error # never | warning | error
|
|
275
|
+
output: human # human | json | prompt
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The action installs Bun, runs the scan, and exposes the report as the `report`
|
|
279
|
+
output so a later step can post it. Or run it directly:
|
|
280
|
+
|
|
281
|
+
```yaml
|
|
282
|
+
- name: agentscan
|
|
283
|
+
run: bunx agentscan check --fail-on error
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Or with a local checkout of this repo:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
bun run src/cli.ts check --fail-on warning --json
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Default `failOn` is `never` so local runs stay non-blocking until you opt in.
|
|
293
|
+
|
|
294
|
+
## Config
|
|
295
|
+
|
|
296
|
+
Optional `.agentscanrc.json` (create with `agentscan init`):
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
"skillPaths": [".agents/skills", ".claude/skills", "skills", ".cursor/skills"],
|
|
301
|
+
"mcpPaths": [".mcp.json", ".claude/mcp.json", "mcp.json"],
|
|
302
|
+
"policyFiles": ["AGENTS.md", "CLAUDE.md"],
|
|
303
|
+
"ignoreSkills": [],
|
|
304
|
+
"ignoreRules": [],
|
|
305
|
+
"ignoreFindings": [],
|
|
306
|
+
"failOn": "never",
|
|
307
|
+
"includeGlobal": false,
|
|
308
|
+
"requireLock": false,
|
|
309
|
+
"thresholds": {
|
|
310
|
+
"skillDescriptionBytes": 16000,
|
|
311
|
+
"skills": 30,
|
|
312
|
+
"mcp": 5,
|
|
313
|
+
"agentsMdLines": 150,
|
|
314
|
+
"claudeMdLines": 200,
|
|
315
|
+
"agents": 8
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Budget rules are **info** (hidden unless `--verbose`) so they do not flood default text or CI with `--fail-on warning`.
|
|
321
|
+
|
|
322
|
+
### Handing findings to an agent
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
agentscan check --output prompt
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
A paste-ready markdown handoff: each item carries the message, why it matters,
|
|
329
|
+
the suggested fix, its evidence, and its finding id. Info-severity findings are
|
|
330
|
+
left out — budgets and hygiene notes are for a maintainer to weigh, not work
|
|
331
|
+
items for an executor.
|
|
332
|
+
|
|
333
|
+
### Getting the report out of the terminal
|
|
334
|
+
|
|
335
|
+
Saving and piping are the shell's job, and it already does them:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
agentscan check --output prompt > handoff.md # save
|
|
339
|
+
agentscan check --output prompt | claude # send to another tool
|
|
340
|
+
agentscan check --json | jq '.findings[]' # filter
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
The clipboard is the one thing no shell does portably, so that one is a flag:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
agentscan check --output prompt --copy
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`--copy` tries `wl-copy`, `xclip`, `xsel`, `pbcopy`, `clip.exe` in turn and uses
|
|
350
|
+
the first that succeeds — being installed is not the same as working, and on a
|
|
351
|
+
headless WSL shell the first two are present and both fail. The report still
|
|
352
|
+
goes to stdout; the "copied" note goes to stderr, so `--copy` composes with
|
|
353
|
+
redirection. A clipboard that cannot be reached prints why and does not change
|
|
354
|
+
the exit code.
|
|
355
|
+
|
|
356
|
+
This is the only place agentscan starts a subprocess, and it runs only under
|
|
357
|
+
`--copy`. It writes to the clipboard and nowhere else.
|
|
358
|
+
|
|
359
|
+
### Suppressing one finding
|
|
360
|
+
|
|
361
|
+
`ignoreRules` disables a check project-wide, which is too blunt when a single
|
|
362
|
+
hook uses an exotic launcher. `ignoreFindings` takes exact finding ids — the
|
|
363
|
+
same strings `agentscan explain` accepts, so the value you already have from
|
|
364
|
+
reading the report is the value you paste:
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
{ "ignoreFindings": ["hook.missing-script:hook:PreToolUse:./bin/wrapper"] }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## What it checks
|
|
371
|
+
|
|
372
|
+
Every check lives in `src/checks/` and runs on every `check`. `agentscan rules`
|
|
373
|
+
lists all of them with their ids; `agentscan explain <id>` details any finding.
|
|
374
|
+
|
|
375
|
+
Most validate one discovered item against its own file on disk. The five
|
|
376
|
+
`budget.*` / `policy.*` entries at the bottom judge aggregate size instead —
|
|
377
|
+
they are all **info**, they are sourced to `docs/spec/thresholds.md`, and they
|
|
378
|
+
are retuned through `thresholds` in `.agentscanrc.json` rather than switched
|
|
379
|
+
off.
|
|
380
|
+
|
|
381
|
+
| id | Severity | Catches |
|
|
382
|
+
|----|----------|---------|
|
|
383
|
+
| `config.unreadable` | error | A config file that is not valid JSON, so whatever it declares is silently not in effect |
|
|
384
|
+
| `hook.missing-script` | error | A registered hook whose script does not exist — it never runs |
|
|
385
|
+
| `hook.unknown-event` | error | A hook registered under an event name that is never dispatched |
|
|
386
|
+
| `agent.missing-frontmatter` | warning | An agent definition with no `---` block |
|
|
387
|
+
| `agent.missing-description` | info | Agent frontmatter has no `description`, so it will not be dispatched on its own |
|
|
388
|
+
| `mcp.no-launch` | error | An MCP server with neither `command` nor `url`; its tools are never available |
|
|
389
|
+
| `mcp.url-without-type` | error | A remote MCP server with a `url` but no `type` — read as stdio, fails, and is skipped |
|
|
390
|
+
| `mcp.hardcoded-secret` | error | A token-shaped literal in MCP config (the value is never echoed back) |
|
|
391
|
+
| `mcp.literal-env` | warning | Long literal `env` values that should be `${VAR}` |
|
|
392
|
+
| `skill.missing-skill-md` | warning | A directory under a skill path with no `SKILL.md` |
|
|
393
|
+
| `skill.missing-frontmatter` | warning | `SKILL.md` with no `---` block |
|
|
394
|
+
| `skill.broken-reference` | warning | The body points at a bundled file that does not exist |
|
|
395
|
+
| `skill.duplicate-description` | warning | Two or more skills carry an identical description, so routing between them is arbitrary |
|
|
396
|
+
| `skill.missing-description` | info | Frontmatter has no `description`, so Claude will not load the skill on its own |
|
|
397
|
+
| `skill.locked-not-installed` | warning | `skills-lock.json` pins a skill that is not on disk |
|
|
398
|
+
| `skill.not-in-lock` | info | A skill on disk that the lockfile does not track — local and unpinned |
|
|
399
|
+
| `skill.description-budget` | info | Skill names + descriptions exceed the startup character budget |
|
|
400
|
+
| `skill.no-lockfile` | info | Skills present with no lockfile at all (only with `requireLock`) |
|
|
401
|
+
|
|
402
|
+
Agent definitions are checked for structure only. An agent's frontmatter `name`
|
|
403
|
+
is a display name — 16 of 34 real files declare one that differs from the
|
|
404
|
+
filename, and nothing keys on the filename — so it is deliberately not compared,
|
|
405
|
+
and a test guards against re-adding that check. Model identifiers and tool names
|
|
406
|
+
are likewise not validated: both would need a hardcoded list, which is the shape
|
|
407
|
+
that already shipped a false error. See [plans/003](plans/003-validate-agent-definitions.md).
|
|
408
|
+
|
|
409
|
+
`skill.broken-reference` reads the body, not just the frontmatter. It looks for
|
|
410
|
+
paths under the conventional bundled directories (`scripts/`, `references/`,
|
|
411
|
+
`assets/`, `templates/`, `examples/`) and resolves each against the skill's own
|
|
412
|
+
directory first, then the repo root — both bases are needed, because of 1674
|
|
413
|
+
references measured across 17 projects, 1645 resolved skill-relative and 12 only
|
|
414
|
+
at the root. Fenced code blocks are stripped first: a path in an example is
|
|
415
|
+
illustration, not a pointer.
|
|
416
|
+
|
|
417
|
+
`hook.missing-script` is deliberately conservative. A command is only resolved
|
|
418
|
+
when it is a single invocation with a path-like argument; shell programs
|
|
419
|
+
(`a && b`, `$(...)`, pipes) are skipped, because a hook written as
|
|
420
|
+
`[ ! -f x ] || node x` already handles the missing file and flagging it would be
|
|
421
|
+
a false positive. `node -e "<code>"` is never treated as a path. Only
|
|
422
|
+
`$CLAUDE_PROJECT_DIR` is expanded — other variables are left alone rather than
|
|
423
|
+
guessed at.
|
|
424
|
+
|
|
425
|
+
Checks are written against the published specs, not against what happens to
|
|
426
|
+
appear in real projects. Two that were written the other way round shipped as
|
|
427
|
+
false positives — a nine-name hook-event list where the spec has 31, and a
|
|
428
|
+
`name` must equal the directory rule that the spec explicitly contradicts
|
|
429
|
+
(`name` is optional and defaults to the directory). Both are gone.
|
|
430
|
+
|
|
431
|
+
Every assumption a check encodes is recorded in **[docs/spec/](docs/spec/)**
|
|
432
|
+
with its source URL, the date it was read, and an honest confidence rating.
|
|
433
|
+
When adding a check, add its spec line there first.
|
|
434
|
+
|
|
435
|
+
## Known limits
|
|
436
|
+
|
|
437
|
+
- **No dependency→skill knowledge.** Nothing here says "you have `next`, you
|
|
438
|
+
should add a `next-*` skill". That needed a hand-maintained registry, and the
|
|
439
|
+
version that existed fired on none of the 17 projects measured, so it was
|
|
440
|
+
removed. Skill provenance comes from `skills-lock.json` instead.
|
|
441
|
+
- **No rule produces `refresh` or `keep`.** Comparing a skill's content against
|
|
442
|
+
the `computedHash` in `skills-lock.json` is the obvious `refresh` source; the
|
|
443
|
+
hash algorithm used by the installing tool is not documented here, and it is
|
|
444
|
+
not reproducible from `SKILL.md` bytes alone, so it is not implemented.
|
|
445
|
+
- **Bun only.** `import.meta.dir` / `import.meta.main`; it will not run on Node.
|
|
446
|
+
- Policy files are read up to 100 KB, so `policyLines` undercounts past that.
|
|
447
|
+
|
|
448
|
+
## Development
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
bun install
|
|
452
|
+
bun run typecheck
|
|
453
|
+
bun test
|
|
454
|
+
bun run src/cli.ts check tests/fixtures/lock-drift
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
| Script | Command |
|
|
458
|
+
|--------|---------|
|
|
459
|
+
| `agentscan` | `bun run src/cli.ts` |
|
|
460
|
+
| `typecheck` | `tsc --noEmit` |
|
|
461
|
+
| `test` | `bun test` |
|
|
462
|
+
| `check` | `bun run src/cli.ts check` |
|
|
463
|
+
|
|
464
|
+
## Design docs
|
|
465
|
+
|
|
466
|
+
**This README is the source of truth for current behavior.** The documents under
|
|
467
|
+
`docs/superpowers/specs/` are the original design and are **superseded** —
|
|
468
|
+
several decisions changed while building: Bun-only runtime, the dep-to-skill map
|
|
469
|
+
and its "orphan" heuristic dropped in favour of `skills-lock.json`, budget
|
|
470
|
+
checks added, structural checks added, and the YAML rule engine deleted
|
|
471
|
+
(`plans/010`). Read them as history, not as behavior.
|
|
472
|
+
|
|
473
|
+
## License
|
|
474
|
+
|
|
475
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chimix/agentscan",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Find broken agent configuration in a project — hooks whose scripts are gone, MCP servers that cannot start, skills that disagree with their lockfile",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agentscan": "./src/cli.ts"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"agent-skills",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"mcp",
|
|
21
|
+
"hooks",
|
|
22
|
+
"lint",
|
|
23
|
+
"config",
|
|
24
|
+
"cli",
|
|
25
|
+
"bun"
|
|
26
|
+
],
|
|
27
|
+
"homepage": "https://github.com/SimaAlexandru99/agentscan#readme",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/SimaAlexandru99/agentscan.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/SimaAlexandru99/agentscan/issues"
|
|
34
|
+
},
|
|
35
|
+
"author": "Sima Alexandru <sima.alex33@gmail.com>",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"preinstall": "node -e \"try{require('child_process').execSync('bun --version',{stdio:'ignore'})}catch{console.error('\\n agentscan runs on Bun. Install it: https://bun.sh\\n (the CLI will not start without it)\\n')}\"",
|
|
38
|
+
"agentscan": "bun run src/cli.ts",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"test": "bun test",
|
|
41
|
+
"check": "bun run src/cli.ts check",
|
|
42
|
+
"spec:check": "bun run scripts/spec-drift.ts"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"bun": ">=1.1.0"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"yaml": "^2.7.0",
|
|
50
|
+
"zod": "^3.24.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/bun": "latest",
|
|
54
|
+
"typescript": "^5.8.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/analyze.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { runChecks } from "./checks/index";
|
|
3
|
+
import { loadConfig } from "./config/load";
|
|
4
|
+
import type { AgentscanConfig } from "./config/schema";
|
|
5
|
+
import { resolveRoot } from "./discover/index";
|
|
6
|
+
import { extractFacts } from "./facts/extract";
|
|
7
|
+
import type { Facts, Finding } from "./facts/types";
|
|
8
|
+
import { sortFindings } from "./report/sort";
|
|
9
|
+
|
|
10
|
+
export type AnalyzeOptions = {
|
|
11
|
+
dir?: string;
|
|
12
|
+
global?: boolean;
|
|
13
|
+
configPath?: string;
|
|
14
|
+
failOn?: AgentscanConfig["failOn"];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type Analysis = {
|
|
18
|
+
root: string;
|
|
19
|
+
/** Set only when the requested directory was not itself the project root. */
|
|
20
|
+
resolvedFrom?: string;
|
|
21
|
+
config: AgentscanConfig;
|
|
22
|
+
facts: Facts;
|
|
23
|
+
findings: Finding[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The one definition of "what is wrong with this project".
|
|
28
|
+
*
|
|
29
|
+
* Every command that reports on a project goes through here — `explain`
|
|
30
|
+
* silently missed every structural finding while it built its own pipeline.
|
|
31
|
+
*/
|
|
32
|
+
export function analyze(options: AnalyzeOptions = {}): Analysis {
|
|
33
|
+
const requested = resolve(options.dir ?? process.cwd());
|
|
34
|
+
const root = resolveRoot(requested);
|
|
35
|
+
const loaded = loadConfig(root, options.configPath);
|
|
36
|
+
|
|
37
|
+
const config: AgentscanConfig = {
|
|
38
|
+
...loaded,
|
|
39
|
+
...(options.failOn !== undefined ? { failOn: options.failOn } : {}),
|
|
40
|
+
...(options.global !== undefined ? { includeGlobal: options.global } : {}),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const includeGlobal = options.global ?? config.includeGlobal;
|
|
44
|
+
const facts = extractFacts(root, config, { includeGlobal });
|
|
45
|
+
|
|
46
|
+
const ignoredSkills = new Set(config.ignoreSkills);
|
|
47
|
+
const ignoredRules = new Set(config.ignoreRules);
|
|
48
|
+
|
|
49
|
+
const structural = runChecks(facts, {
|
|
50
|
+
requireLock: config.requireLock,
|
|
51
|
+
skillDescriptionBytes: config.thresholds.skillDescriptionBytes,
|
|
52
|
+
budgets: {
|
|
53
|
+
agentsMdLines: config.thresholds.agentsMdLines,
|
|
54
|
+
claudeMdLines: config.thresholds.claudeMdLines,
|
|
55
|
+
agents: config.thresholds.agents,
|
|
56
|
+
mcp: config.thresholds.mcp,
|
|
57
|
+
},
|
|
58
|
+
}).filter(
|
|
59
|
+
(f) =>
|
|
60
|
+
!ignoredRules.has(f.ruleId) &&
|
|
61
|
+
!(
|
|
62
|
+
f.subject.startsWith("skill:") &&
|
|
63
|
+
ignoredSkills.has(f.subject.slice("skill:".length))
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Suppression by exact finding id. Without it a single false positive costs
|
|
68
|
+
// an entire check via ignoreRules — and under the README's CI recipe that is
|
|
69
|
+
// the only way to get a build green again.
|
|
70
|
+
const ignoredFindings = new Set(config.ignoreFindings);
|
|
71
|
+
const findings = sortFindings(
|
|
72
|
+
structural.filter((f) => !ignoredFindings.has(f.id)),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
root,
|
|
77
|
+
...(root === requested ? {} : { resolvedFrom: requested }),
|
|
78
|
+
config,
|
|
79
|
+
facts,
|
|
80
|
+
findings,
|
|
81
|
+
};
|
|
82
|
+
}
|