@jqntn/agentdoctor 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 +215 -0
- package/bin/agentdoctor.js +314 -0
- package/docs/agents.md +119 -0
- package/docs/api.md +100 -0
- package/docs/architecture.md +90 -0
- package/docs/baselines.md +56 -0
- package/docs/ci.md +87 -0
- package/docs/configuration.md +115 -0
- package/docs/faq.md +83 -0
- package/docs/getting-started.md +99 -0
- package/docs/output.md +97 -0
- package/docs/policy.md +94 -0
- package/docs/rules.md +463 -0
- package/package.json +71 -0
- package/schemas/policy.schema.json +36 -0
- package/schemas/report.schema.json +58 -0
- package/skills/config-audit/SKILL.md +72 -0
- package/skills/config-audit/references/fix-recipes.md +107 -0
- package/src/adopt.js +178 -0
- package/src/constants.js +139 -0
- package/src/discover.js +235 -0
- package/src/engine.js +218 -0
- package/src/grade.js +39 -0
- package/src/index.js +42 -0
- package/src/links.js +9 -0
- package/src/parse.js +318 -0
- package/src/report/json.js +36 -0
- package/src/report/sarif.js +68 -0
- package/src/report/terminal.js +135 -0
- package/src/rules/correctness.js +849 -0
- package/src/rules/cost.js +282 -0
- package/src/rules/hygiene.js +199 -0
- package/src/rules/index.js +18 -0
- package/src/rules/policy.js +288 -0
- package/src/rules/security.js +690 -0
package/docs/api.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Programmatic API
|
|
2
|
+
|
|
3
|
+
agentdoctor is an ES module with no dependencies, so embedding it is one import.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { run } from '@jqntn/agentdoctor';
|
|
7
|
+
|
|
8
|
+
const result = run('/path/to/repo', { includeUserScope: false });
|
|
9
|
+
|
|
10
|
+
for (const finding of result.findings) {
|
|
11
|
+
console.log(finding.severity, finding.ruleId, `${finding.display}:${finding.line}`);
|
|
12
|
+
}
|
|
13
|
+
process.exitCode = result.findings.some((f) => f.severity === 'error') ? 1 : 0;
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## `run(root, options?)`
|
|
17
|
+
|
|
18
|
+
The one-call entry point: discovers config, loads any team policy, runs every rule.
|
|
19
|
+
|
|
20
|
+
| Option | Type | Default | Effect |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `includeUserScope` | boolean | `true` | Also scan `~/.claude` |
|
|
23
|
+
| `home` | string | `os.homedir()` | Override the home directory (useful in tests) |
|
|
24
|
+
| `policyPath` | string | auto-detect | Explicit policy file path |
|
|
25
|
+
| `only` | string[] | all | Restrict to categories or rule ids |
|
|
26
|
+
| `disabled` | string[] | none | Skip rules or categories |
|
|
27
|
+
| `minSeverity` | `'error'\|'warning'\|'info'` | `'info'` | Severity floor |
|
|
28
|
+
| `baseline` | `Set<string>` | empty | Fingerprints to suppress |
|
|
29
|
+
|
|
30
|
+
Returns `{ findings, ran, suppressed, workspace, elapsedMs, version }`.
|
|
31
|
+
|
|
32
|
+
### Finding shape
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
{
|
|
36
|
+
ruleId: string; // e.g. "security/unrestricted-bash"
|
|
37
|
+
severity: 'error' | 'warning' | 'info';
|
|
38
|
+
category: 'correctness' | 'security' | 'cost' | 'hygiene' | 'policy';
|
|
39
|
+
message: string; // what is wrong, specific to this occurrence
|
|
40
|
+
help?: string; // what to do instead, from the rule
|
|
41
|
+
file: string; // absolute path
|
|
42
|
+
display: string; // repo-relative (or ~/) path for humans
|
|
43
|
+
line: number; // 1-based
|
|
44
|
+
column?: number;
|
|
45
|
+
configPath?: string; // e.g. "permissions.allow[0]"
|
|
46
|
+
snippet?: string; // offending value, secrets redacted
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Findings are pre-sorted by severity, then file, then line.
|
|
51
|
+
|
|
52
|
+
## Lower-level building blocks
|
|
53
|
+
|
|
54
|
+
All exported from the package root:
|
|
55
|
+
|
|
56
|
+
- `discover(root, { includeUserScope, home })` — collects and parses every config file, with
|
|
57
|
+
per-value source positions. Never opens credential files.
|
|
58
|
+
- `lint(workspace, { rules, disabled, minSeverity, baseline })` — runs rules over a discovered
|
|
59
|
+
workspace. Pass your own `rules` array to run a custom subset or add your own rules.
|
|
60
|
+
- `fingerprint(finding)` — the stable identity used by baselines. Anchored to the offending
|
|
61
|
+
value, then config path, then message hash — never line numbers.
|
|
62
|
+
- `allRules`, `CATEGORIES` — the catalogue.
|
|
63
|
+
- `loadPolicy(root, explicitPath?)` — reads `agentdoctor.policy.json`.
|
|
64
|
+
- `helpers` — utilities handed to rules: `parsePermission`, `estimateTokens`, position lookup.
|
|
65
|
+
|
|
66
|
+
## Writing a custom rule
|
|
67
|
+
|
|
68
|
+
A rule is a plain object; `lint` accepts any array of them.
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import { discover, lint, allRules, helpers } from '@jqntn/agentdoctor';
|
|
72
|
+
|
|
73
|
+
const noOpusInProjects = {
|
|
74
|
+
id: 'org/no-opus-model',
|
|
75
|
+
category: 'policy',
|
|
76
|
+
severity: 'warning',
|
|
77
|
+
title: 'Project pins an Opus-tier model',
|
|
78
|
+
help: 'Our org standard is sonnet for project config; sessions can override per run.',
|
|
79
|
+
check({ files, report, helpers }) {
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
if (file.kind !== 'settings' || file.data?.model !== 'opus') continue;
|
|
82
|
+
const position = helpers.at(file, 'model');
|
|
83
|
+
report({ file, line: position.line, column: position.column,
|
|
84
|
+
configPath: 'model', message: 'model is pinned to opus.' });
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const workspace = discover(process.cwd(), { includeUserScope: false });
|
|
90
|
+
const result = lint(workspace, { rules: [...allRules, noOpusInProjects] });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The `check` function receives `{ workspace, files, report, helpers }`. Throwing inside a rule
|
|
94
|
+
does not crash the run — it surfaces as an `internal/rule-crashed` warning finding.
|
|
95
|
+
|
|
96
|
+
## Stability
|
|
97
|
+
|
|
98
|
+
The exported API surface is small on purpose: `run`, `discover`, `lint`, `fingerprint`,
|
|
99
|
+
`allRules`, `CATEGORIES`, `loadPolicy`, `helpers`, `VERSION`. Anything not exported from the
|
|
100
|
+
package root is internal and may change without notice.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Four stages, each a plain module with no dependencies: **discover → parse → lint → report**.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
bin/agentdoctor.js CLI: flags, exit codes, EPIPE handling
|
|
7
|
+
src/
|
|
8
|
+
discover.js finds config files; never opens credential files
|
|
9
|
+
parse.js position-tracking JSON + frontmatter parsers
|
|
10
|
+
engine.js runs rules, suppression, baselines, fingerprints
|
|
11
|
+
rules/
|
|
12
|
+
correctness.js (26) config the harness silently ignores
|
|
13
|
+
security.js (22) config that widens the execution surface
|
|
14
|
+
cost.js (8) always-on context, priced with stated assumptions
|
|
15
|
+
hygiene.js (8) config that confuses the next person
|
|
16
|
+
policy.js (8) team standards from agentdoctor.policy.json
|
|
17
|
+
report/
|
|
18
|
+
terminal.js json.js sarif.js
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Discovery
|
|
22
|
+
|
|
23
|
+
`discover()` walks the project for every file the agent harness reads: settings at three
|
|
24
|
+
scopes (project, local, user), `.mcp.json`, memory files at any depth, agents, skills,
|
|
25
|
+
commands, hooks, keybindings. Vendor directories (`node_modules`, `dist`, `.venv`, …) are
|
|
26
|
+
skipped, walk depth is capped, and files over 4 MB are recorded as skipped rather than read.
|
|
27
|
+
|
|
28
|
+
Two properties are deliberate and load-bearing:
|
|
29
|
+
|
|
30
|
+
- **Credential files are never opened.** `.credentials.json`, `.netrc`, private keys are
|
|
31
|
+
excluded by *path* before any `readFile`, and a test asserts a tripwire value planted in a
|
|
32
|
+
credentials file can never reach output. A security tool's own behavior is part of its
|
|
33
|
+
threat model.
|
|
34
|
+
- **No network calls, ever.** Not for updates, not for telemetry. The entire run is local
|
|
35
|
+
file reads.
|
|
36
|
+
|
|
37
|
+
## Position-tracking parsing
|
|
38
|
+
|
|
39
|
+
Findings are only actionable if they point at a line, so agentdoctor does not use
|
|
40
|
+
`JSON.parse`. `src/parse.js` is a hand-written JSON parser that records the `line:column` of
|
|
41
|
+
every value, keyed by config path (`permissions.allow[0]`). It is also deliberately tolerant:
|
|
42
|
+
trailing commas and comments — common in hand-edited config — parse fine, so one stray comma
|
|
43
|
+
yields real findings instead of a single parse error. A genuinely broken file becomes a
|
|
44
|
+
`correctness/invalid-json` **error**, because the harness ignores the entire file in that
|
|
45
|
+
case, including any permission rules in it.
|
|
46
|
+
|
|
47
|
+
Agent and skill definitions carry config in YAML frontmatter. The frontmatter parser supports
|
|
48
|
+
the documented subset (scalars, inline and dash lists, one nesting level) rather than taking a
|
|
49
|
+
YAML dependency.
|
|
50
|
+
|
|
51
|
+
## The rule engine
|
|
52
|
+
|
|
53
|
+
A rule is a plain object: `{ id, category, severity, title, help, check() }`. The engine calls
|
|
54
|
+
each rule with the workspace and a `report()` callback, then handles everything rules should
|
|
55
|
+
not re-implement:
|
|
56
|
+
|
|
57
|
+
- **Suppression** — inline `agentdoctor-disable` comments, `--disable`, `--only`,
|
|
58
|
+
`--min-severity`, and baselines all apply centrally.
|
|
59
|
+
- **Fingerprints** — each finding gets a stable identity anchored to the offending value,
|
|
60
|
+
then config path, then message hash. Never line numbers: a baseline must survive unrelated
|
|
61
|
+
edits ([why](baselines.md)).
|
|
62
|
+
- **Crash isolation** — a throwing rule becomes an `internal/rule-crashed` warning; it cannot
|
|
63
|
+
take the run down.
|
|
64
|
+
- **Ordering** — findings sort by severity, file, line, so output is deterministic.
|
|
65
|
+
|
|
66
|
+
## Design principles
|
|
67
|
+
|
|
68
|
+
1. **False positives are worse than false negatives.** A linter that cries wolf gets
|
|
69
|
+
uninstalled, at which point it catches nothing. The test suite contains a fully
|
|
70
|
+
well-configured fixture project that must produce **zero** findings; any rule that fires on
|
|
71
|
+
it is wrong by definition.
|
|
72
|
+
2. **Every finding says why and what to do.** `message` states the specific problem; `help`
|
|
73
|
+
states the fix and the reasoning. `--explain <rule-id>` prints the full rationale.
|
|
74
|
+
3. **Silent failure is the enemy.** The highest-value rules are the ones that catch config
|
|
75
|
+
the harness ignores without any error: misspelled hook events, deny rules naming
|
|
76
|
+
nonexistent tools, hooks pointing at missing scripts.
|
|
77
|
+
4. **Zero dependencies, permanently.** This tool warns about supply-chain risk in MCP
|
|
78
|
+
servers; its own `npm install` footprint is part of the product. The JSON parser, YAML
|
|
79
|
+
subset, ANSI styling, and SARIF writer are all in-tree.
|
|
80
|
+
5. **Estimates state their assumptions.** Cost rules price always-on context using a model
|
|
81
|
+
that accounts for prompt caching, and say so in the message. The token count is the fact;
|
|
82
|
+
the money is a model.
|
|
83
|
+
|
|
84
|
+
## Testing
|
|
85
|
+
|
|
86
|
+
`node --test`, no framework. The suite covers every rule (a meta-test fails if a rule id
|
|
87
|
+
appears in no test file), parser positions, discovery, baselines (insertion-stability
|
|
88
|
+
regression tests), CLI behavior including piping and exit codes, and docs consistency — the
|
|
89
|
+
README's rule counts are asserted against the actual catalogue so marketing copy cannot drift
|
|
90
|
+
from the code.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Baselines: adopting agentdoctor on an existing repo
|
|
2
|
+
|
|
3
|
+
An established project will have findings on day one. Fixing all of them before turning on CI
|
|
4
|
+
enforcement is how adoption dies. A baseline records the current findings as accepted, so CI
|
|
5
|
+
fails only on **new** problems.
|
|
6
|
+
|
|
7
|
+
## Workflow
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
# once, reviewed and committed like any other change
|
|
11
|
+
agentdoctor --no-user --write-baseline .agentdoctor-baseline.json
|
|
12
|
+
git add .agentdoctor-baseline.json
|
|
13
|
+
|
|
14
|
+
# in CI, from then on
|
|
15
|
+
agentdoctor --no-user --baseline .agentdoctor-baseline.json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Fix things over time, then shrink the baseline by regenerating it:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
agentdoctor --no-user --write-baseline .agentdoctor-baseline.json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The file is plain JSON — a list of finding fingerprints — so shrinkage is visible in diffs and
|
|
25
|
+
code review. A growing baseline is a red flag a reviewer can see.
|
|
26
|
+
|
|
27
|
+
## Why baselines survive edits
|
|
28
|
+
|
|
29
|
+
A baseline that breaks when someone inserts an unrelated line is a baseline people stop
|
|
30
|
+
trusting. agentdoctor anchors each fingerprint to the most stable identity available, in order:
|
|
31
|
+
|
|
32
|
+
1. **The offending value itself** (e.g. the text of the permission rule). Permission rules
|
|
33
|
+
live in arrays, so a positional anchor like `permissions.allow[0]` changes meaning the
|
|
34
|
+
moment anyone inserts a rule above it. The rule text does not.
|
|
35
|
+
2. **The config path**, for structural findings with no single value (an empty deny list, a
|
|
36
|
+
missing required key).
|
|
37
|
+
3. **A hash of the finding's message**, for whole-file findings.
|
|
38
|
+
|
|
39
|
+
Line numbers are never part of the identity. Concretely:
|
|
40
|
+
|
|
41
|
+
- Insert a new (bad) rule anywhere in the allow list → exactly one new finding surfaces.
|
|
42
|
+
- Add unrelated keys above the offending line → nothing resurfaces.
|
|
43
|
+
- Fix a finding → its fingerprint disappears on the next `--write-baseline`.
|
|
44
|
+
|
|
45
|
+
Each of these is a regression test in the suite (`test/baseline.test.js`).
|
|
46
|
+
|
|
47
|
+
## Baselines vs inline suppression
|
|
48
|
+
|
|
49
|
+
| | Baseline | `agentdoctor-disable` comment |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| Scope | Whole project, one file | One rule, one file |
|
|
52
|
+
| Visibility | A count in every summary + a diffable JSON file | A comment next to the code |
|
|
53
|
+
| Use for | The adoption backlog | A deliberate, permanent exception |
|
|
54
|
+
|
|
55
|
+
Rule of thumb: baselines are for *debt*, inline suppressions are for *decisions*. If you find
|
|
56
|
+
yourself regenerating the baseline to absorb new findings, you have turned the fire alarm off.
|
package/docs/ci.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Running agentdoctor in CI
|
|
2
|
+
|
|
3
|
+
## GitHub Actions, as code scanning annotations
|
|
4
|
+
|
|
5
|
+
Findings appear inline on the pull request diff.
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
name: agentdoctor
|
|
9
|
+
|
|
10
|
+
on: [pull_request]
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
security-events: write
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
audit:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-node@v4
|
|
22
|
+
with: { node-version: 22 }
|
|
23
|
+
|
|
24
|
+
# continue-on-error so the SARIF still uploads when findings exist;
|
|
25
|
+
# the gate job below is what actually fails the build.
|
|
26
|
+
- run: npx @jqntn/agentdoctor --no-user --sarif > agentdoctor.sarif
|
|
27
|
+
continue-on-error: true
|
|
28
|
+
|
|
29
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
30
|
+
with:
|
|
31
|
+
sarif_file: agentdoctor.sarif
|
|
32
|
+
|
|
33
|
+
- name: Fail on errors
|
|
34
|
+
run: npx @jqntn/agentdoctor --no-user --quiet
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`--no-user` matters in CI: there is no `~/.claude` on a runner, and scanning it locally would
|
|
38
|
+
report findings a reviewer cannot act on.
|
|
39
|
+
|
|
40
|
+
## Any other CI
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npx @jqntn/agentdoctor --no-user --json > agentdoctor.json # exit 1 if errors exist
|
|
44
|
+
npx @jqntn/agentdoctor --no-user --max-warnings 0 # also fail on warnings
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Exit codes are the contract:
|
|
48
|
+
|
|
49
|
+
| Code | Meaning |
|
|
50
|
+
|---|---|
|
|
51
|
+
| 0 | No errors (and warnings within `--max-warnings`) |
|
|
52
|
+
| 1 | At least one error, or too many warnings |
|
|
53
|
+
| 2 | Bad usage: unknown flag, missing path, unreadable baseline |
|
|
54
|
+
|
|
55
|
+
## Adopting on a repo that already has findings
|
|
56
|
+
|
|
57
|
+
Fail on new problems without having to fix the backlog first:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
# once, on a green-ish commit
|
|
61
|
+
npx @jqntn/agentdoctor --no-user --write-baseline .agentdoctor-baseline.json
|
|
62
|
+
git add .agentdoctor-baseline.json
|
|
63
|
+
|
|
64
|
+
# in CI, from then on
|
|
65
|
+
npx @jqntn/agentdoctor --no-user --baseline .agentdoctor-baseline.json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Baseline entries are fingerprints of `rule id + file + config path`, so moving a rule within a
|
|
69
|
+
file keeps it suppressed, while adding a genuinely new one does not.
|
|
70
|
+
|
|
71
|
+
Shrink the baseline as you fix things:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npx @jqntn/agentdoctor --no-user --write-baseline .agentdoctor-baseline.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Enforcing one standard across many repos
|
|
78
|
+
|
|
79
|
+
Commit the same `agentdoctor.policy.json` to every repo, or fetch it from a central location in
|
|
80
|
+
CI, and the policy rules hold each repo to it:
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
- run: curl -sSf https://internal.example.com/agentdoctor.policy.json -o agentdoctor.policy.json
|
|
84
|
+
- run: npx @jqntn/agentdoctor --no-user --quiet
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The policy rules activate on the presence of the file — nothing else to configure.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
agentdoctor needs no config file to run. Everything is a CLI flag, an inline comment, or (for
|
|
4
|
+
team standards) an `agentdoctor.policy.json`.
|
|
5
|
+
|
|
6
|
+
## CLI reference
|
|
7
|
+
|
|
8
|
+
### Output
|
|
9
|
+
|
|
10
|
+
| Flag | Effect |
|
|
11
|
+
|---|---|
|
|
12
|
+
| *(default)* | Human-readable report, colored when stdout is a TTY |
|
|
13
|
+
| `--json` | Machine-readable findings on stdout ([format](output.md)) |
|
|
14
|
+
| `--sarif` | SARIF 2.1.0 for GitHub code scanning and other CI |
|
|
15
|
+
| `--quiet`, `-q` | Print nothing; rely on the exit code |
|
|
16
|
+
| `--no-color` / `--color` | Force color off/on (also honours `NO_COLOR` and `FORCE_COLOR`) |
|
|
17
|
+
|
|
18
|
+
### Scope
|
|
19
|
+
|
|
20
|
+
| Flag | Effect |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `[path]` | Project root to audit (default: current directory) |
|
|
23
|
+
| `--no-user` | Skip `~/.claude`. Recommended in CI, where user scope does not exist |
|
|
24
|
+
| `--only <cat,...>` | Run only these categories or rule ids |
|
|
25
|
+
| `--disable <id,...>` | Skip specific rules or whole categories |
|
|
26
|
+
| `--min-severity <level>` | `error`, `warning`, or `info` (default) |
|
|
27
|
+
|
|
28
|
+
Categories: `correctness`, `security`, `cost`, `hygiene`, `policy`.
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
agentdoctor --only security,correctness
|
|
32
|
+
agentdoctor --disable cost/no-cleanup-period,hygiene
|
|
33
|
+
agentdoctor --min-severity warning
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### CI
|
|
37
|
+
|
|
38
|
+
| Flag | Effect |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `--max-warnings <n>` | Exit 1 if more than n warnings (errors always exit 1) |
|
|
41
|
+
| `--baseline <file>` | Suppress findings recorded in the baseline ([guide](baselines.md)) |
|
|
42
|
+
| `--write-baseline <file>` | Record current findings as accepted |
|
|
43
|
+
|
|
44
|
+
### Team policy
|
|
45
|
+
|
|
46
|
+
| Flag | Effect |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `--policy <file>` | Policy file path (default: `agentdoctor.policy.json` at the root) |
|
|
49
|
+
| `--init-policy` | Write a starter policy file ([guide](policy.md)) |
|
|
50
|
+
|
|
51
|
+
### Adopt & share
|
|
52
|
+
|
|
53
|
+
| Flag | Effect |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `--init-ci` | Write `.github/workflows/agentdoctor.yml`: SARIF annotations + exit-code gate. Refuses to overwrite. |
|
|
56
|
+
| `--init-skill` | Install the config-audit skill (SKILL.md + fix recipes) for Claude Code. Refuses to overwrite. |
|
|
57
|
+
| `--init-agents` | Add a marked audit section to `AGENTS.md` for Codex, Cursor, Gemini CLI and every other tool that reads it. Creates or appends; refuses to duplicate. |
|
|
58
|
+
| `--badge` | Print README markdown for a badge showing the current grade |
|
|
59
|
+
| `--share` | Print a paste-ready score card: grade, counts, top rule ids. Never includes messages, paths, or snippets, so it is safe to share from private repos. Always exits 0. |
|
|
60
|
+
|
|
61
|
+
### Introspection
|
|
62
|
+
|
|
63
|
+
| Flag | Effect |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `--list-rules` | The full catalogue (add `--json` for machine-readable) |
|
|
66
|
+
| `--explain <rule-id>` | What a rule checks, why it matters, how to suppress it |
|
|
67
|
+
| `--version`, `--help` | The usual |
|
|
68
|
+
|
|
69
|
+
## Suppressing a rule for one file
|
|
70
|
+
|
|
71
|
+
Put a comment anywhere in the offending file:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
// agentdoctor-disable security/hook-unpinned-path
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
In JSON config, a comment works (agentdoctor's parser tolerates comments) — or use a string
|
|
78
|
+
key that contains the directive:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"// agentdoctor-disable security/hook-unpinned-path": "hooks come from vendored bin/",
|
|
83
|
+
"hooks": { }
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Accepted forms:
|
|
88
|
+
|
|
89
|
+
- `agentdoctor-disable <rule-id>` — one rule
|
|
90
|
+
- `agentdoctor-disable <category>` — a whole category
|
|
91
|
+
- `agentdoctor-disable all` — everything, for this file
|
|
92
|
+
- Multiple ids separated by commas or spaces
|
|
93
|
+
|
|
94
|
+
Suppressions are file-scoped by design: a suppression you can see next to the code it affects
|
|
95
|
+
is one a reviewer can question.
|
|
96
|
+
|
|
97
|
+
To mark a credential-looking string as a deliberate placeholder:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
agentdoctor-allow-secret
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
on the same line as the value.
|
|
104
|
+
|
|
105
|
+
## Precedence
|
|
106
|
+
|
|
107
|
+
1. `permissions.deny`-style hard skips: credential files are never read, regardless of flags.
|
|
108
|
+
2. `--only` narrows the rule set first.
|
|
109
|
+
3. `--disable` removes rules or categories from whatever `--only` left.
|
|
110
|
+
4. Inline `agentdoctor-disable` comments suppress findings per file.
|
|
111
|
+
5. `--baseline` suppresses previously accepted findings.
|
|
112
|
+
6. `--min-severity` filters what is left.
|
|
113
|
+
|
|
114
|
+
Suppressed counts are always reported in the summary, so a silenced finding is visible as a
|
|
115
|
+
number even when its detail is not.
|
package/docs/faq.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# FAQ
|
|
2
|
+
|
|
3
|
+
## Is my config uploaded anywhere?
|
|
4
|
+
|
|
5
|
+
No. agentdoctor makes zero network calls — no telemetry, no update checks, no license pings.
|
|
6
|
+
The entire run is local file reads. It also never opens credential files
|
|
7
|
+
(`.credentials.json`, `.netrc`, private keys); they are excluded by path before anything
|
|
8
|
+
reads them, and the summary reports how many were skipped.
|
|
9
|
+
|
|
10
|
+
## Why did it find nothing?
|
|
11
|
+
|
|
12
|
+
Probably because your config is small. A 10-line `settings.json` with two permission rules
|
|
13
|
+
has little to get wrong, and agentdoctor is deliberately quiet on healthy setups — the test
|
|
14
|
+
suite asserts zero findings on a well-configured project. The findings density rises with
|
|
15
|
+
hooks, MCP servers, subagents, skills, and memory files.
|
|
16
|
+
|
|
17
|
+
## Isn't this just a JSON schema?
|
|
18
|
+
|
|
19
|
+
A schema catches type errors. It cannot tell you that `Bash(*)` is a bad idea, that your hook
|
|
20
|
+
script does not exist on disk, that your deny rule names a tool that does not exist (and
|
|
21
|
+
therefore blocks nothing), or that your `CLAUDE.md` costs real money per month. Most of the
|
|
22
|
+
catalogue is semantic, not structural.
|
|
23
|
+
|
|
24
|
+
## Why do you report warnings on things that are technically legal?
|
|
25
|
+
|
|
26
|
+
Because the failure mode this tool exists for is config that is *legal and inert*. A
|
|
27
|
+
misspelled hook event is valid JSON. It just never fires, and nothing tells you. When
|
|
28
|
+
agentdoctor is unsure, it says `info`; when something is legal but almost certainly not what
|
|
29
|
+
you meant, `warning`; when a guardrail provably does nothing or a real hazard is
|
|
30
|
+
pre-approved, `error`.
|
|
31
|
+
|
|
32
|
+
## A rule fired on something intentional. What now?
|
|
33
|
+
|
|
34
|
+
Suppress it where it fired, visibly:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
// agentdoctor-disable security/hook-unpinned-path
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If you believe the rule is wrong in general, that is a bug — false positives are treated as
|
|
41
|
+
more severe than false negatives. Open an issue with the config that triggered it.
|
|
42
|
+
|
|
43
|
+
## How accurate are the cost estimates?
|
|
44
|
+
|
|
45
|
+
The token counts are direct estimates from file size. The money figures are a *model*, and the
|
|
46
|
+
message says so: they assume the memory file stays prompt-cached (it sits at the front of the
|
|
47
|
+
prompt, which is exactly the content that caches) and state the request volume they assume.
|
|
48
|
+
The uncached worst case is also shown where it matters. Assumptions live in
|
|
49
|
+
`src/rules/cost.js` where you can disagree with them.
|
|
50
|
+
|
|
51
|
+
## Does it modify my config?
|
|
52
|
+
|
|
53
|
+
No. agentdoctor reports; you decide. Every finding includes what to change and why, but the
|
|
54
|
+
edit is yours. The only file it ever writes is a baseline, and only when you pass
|
|
55
|
+
`--write-baseline`.
|
|
56
|
+
|
|
57
|
+
## Which harnesses does it understand?
|
|
58
|
+
|
|
59
|
+
The `.claude/` configuration surface (Claude Code and compatible tooling), `.mcp.json` MCP
|
|
60
|
+
server definitions, and the `CLAUDE.md`/`AGENTS.md` memory-file convention. The rule engine is
|
|
61
|
+
harness-agnostic — discovery is the only layer that knows file layouts — so support for other
|
|
62
|
+
agent config formats is an issue away.
|
|
63
|
+
|
|
64
|
+
## Does it work on Windows?
|
|
65
|
+
|
|
66
|
+
Yes, with one deliberate gap: the two rules that inspect file permissions
|
|
67
|
+
(`security/world-writable-config` and the permission half of
|
|
68
|
+
`security/hook-script-not-executable`) do nothing on Windows. Node synthesizes POSIX mode bits
|
|
69
|
+
there — every file reports `0666` — so the check would fire on everything while telling you
|
|
70
|
+
nothing. Windows ACLs are a different model than this rule can speak to. Every other rule
|
|
71
|
+
behaves identically across Linux, macOS and Windows, and paths in output always use forward
|
|
72
|
+
slashes so a baseline recorded on one platform matches on another.
|
|
73
|
+
|
|
74
|
+
## Why Node 20+? Why zero dependencies?
|
|
75
|
+
|
|
76
|
+
Node 20 is the oldest LTS with everything the tool needs built in. Zero dependencies is a
|
|
77
|
+
security decision, not an aesthetic one: a tool that warns you about unpinned supply chains
|
|
78
|
+
should not install one.
|
|
79
|
+
|
|
80
|
+
## Can I use it as a library?
|
|
81
|
+
|
|
82
|
+
Yes — `import { run } from '@jqntn/agentdoctor'` and you get structured findings. See the
|
|
83
|
+
[API docs](api.md), including how to add organisation-specific rules.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
Node 20 or newer. Nothing else — agentdoctor has zero dependencies and installs no transitive
|
|
6
|
+
packages.
|
|
7
|
+
|
|
8
|
+
## Run it
|
|
9
|
+
|
|
10
|
+
No install needed:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx @jqntn/agentdoctor
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or install it:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install -g @jqntn/agentdoctor # global CLI
|
|
20
|
+
npm install -D @jqntn/agentdoctor # per-project, for CI
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
By default it audits the current directory plus your user-level config in `~/.claude`. To audit
|
|
24
|
+
a specific project, pass the path:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
agentdoctor path/to/repo
|
|
28
|
+
agentdoctor --no-user # project config only (use this in CI)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## What gets scanned
|
|
32
|
+
|
|
33
|
+
| File | What it is |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `.claude/settings.json` | Project settings: permissions, hooks, env, model |
|
|
36
|
+
| `.claude/settings.local.json` | Personal overrides (should be gitignored) |
|
|
37
|
+
| `~/.claude/settings.json` | User-level settings |
|
|
38
|
+
| `.mcp.json` | MCP server definitions |
|
|
39
|
+
| `CLAUDE.md`, `CLAUDE.local.md`, `AGENTS.md` | Memory files, at any depth |
|
|
40
|
+
| `.claude/agents/*.md` | Subagent definitions |
|
|
41
|
+
| `.claude/skills/*/SKILL.md` | Skill definitions |
|
|
42
|
+
| `.claude/commands/*.md` | Slash commands |
|
|
43
|
+
| `.claude/hooks/*` | Hook scripts (existence and permissions only) |
|
|
44
|
+
| `.claude/keybindings.json` | Key bindings |
|
|
45
|
+
|
|
46
|
+
**Never scanned:** `.credentials.json`, `.netrc`, private keys. These are skipped by path
|
|
47
|
+
before anything opens them, and the summary reports how many files were skipped. agentdoctor
|
|
48
|
+
also makes no network calls — nothing leaves your machine.
|
|
49
|
+
|
|
50
|
+
## Reading a finding
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
.claude/settings.json
|
|
54
|
+
4:7 error "Bash(*)" auto-approves every shell command, including ones
|
|
55
|
+
you have not seen.
|
|
56
|
+
| Bash(*)
|
|
57
|
+
-> Replace the wildcard with the specific commands you actually
|
|
58
|
+
want unattended, e.g. "Bash(npm test:*)".
|
|
59
|
+
security/unrestricted-bash
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Top to bottom: file, `line:column`, severity, what is wrong, the offending value, what to do
|
|
63
|
+
instead, and the rule id. Every rule id works with `--explain`:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
agentdoctor --explain security/unrestricted-bash
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## The grade
|
|
70
|
+
|
|
71
|
+
Every report ends with a health grade - `A+` (zero findings), `A` (info only), `B`/`C`
|
|
72
|
+
(warnings), `D`/`F` (errors). It is computed from what is actionable today, so fixing or
|
|
73
|
+
deliberately suppressing findings raises it. `agentdoctor --share` prints a paste-ready
|
|
74
|
+
score card (rule ids and counts only - safe to share from private repos), and
|
|
75
|
+
`agentdoctor --badge` emits README markdown for it.
|
|
76
|
+
|
|
77
|
+
## Severities
|
|
78
|
+
|
|
79
|
+
| Severity | Meaning |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `error` | Broken or dangerous. A guardrail that does not work, a pre-approved destructive command, a committed credential. |
|
|
82
|
+
| `warning` | Very likely a problem, occasionally intentional. |
|
|
83
|
+
| `info` | Worth knowing; act on it or ignore it. |
|
|
84
|
+
|
|
85
|
+
## Exit codes
|
|
86
|
+
|
|
87
|
+
| Code | Meaning |
|
|
88
|
+
|---|---|
|
|
89
|
+
| 0 | No errors (and warnings within `--max-warnings`, if set) |
|
|
90
|
+
| 1 | At least one error, or too many warnings |
|
|
91
|
+
| 2 | Bad usage: unknown flag, missing path, unreadable baseline |
|
|
92
|
+
|
|
93
|
+
## Next steps
|
|
94
|
+
|
|
95
|
+
- [Configuration](configuration.md) — every flag, suppression, disabling rules
|
|
96
|
+
- [CI setup](ci.md) — SARIF annotations, exit-code gating
|
|
97
|
+
- [Baselines](baselines.md) — adopting agentdoctor on a repo that already has findings
|
|
98
|
+
- [Team policy](policy.md) — holding many repos to one standard
|
|
99
|
+
- [Rule reference](rules.md) — all 72 rules and the reasoning behind each
|