@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the agentdoctor authors
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,215 @@
1
+ <div align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jqntn/agentdoctor/main/assets/logo-dark.svg">
4
+ <img src="https://raw.githubusercontent.com/jqntn/agentdoctor/main/assets/logo-light.svg" alt="agentdoctor" width="420">
5
+ </picture>
6
+
7
+ <p><strong>Lint your AI coding agent's configuration before it bites.</strong></p>
8
+
9
+ <p>
10
+ <a href="https://github.com/jqntn/agentdoctor/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/jqntn/agentdoctor/actions/workflows/ci.yml/badge.svg"></a>
11
+ <a href="https://www.npmjs.com/package/@jqntn/agentdoctor"><img alt="npm" src="https://img.shields.io/npm/v/%40jqntn%2Fagentdoctor"></a>
12
+ <img alt="zero dependencies" src="https://img.shields.io/badge/dependencies-0-34D399">
13
+ <img alt="node >=20" src="https://img.shields.io/badge/node-%3E%3D20-64748B">
14
+ <a href="LICENSE"><img alt="MIT" src="https://img.shields.io/badge/license-MIT-blue"></a>
15
+ </p>
16
+
17
+ <p>
18
+ <a href="https://jqntn.github.io/agentdoctor/">Website</a> ·
19
+ <a href="docs/getting-started.md">Getting started</a> ·
20
+ <a href="docs/rules.md">Rule reference</a> ·
21
+ <a href="docs/ci.md">CI setup</a> ·
22
+ <a href="docs/agents.md">For agents</a>
23
+ </p>
24
+ </div>
25
+
26
+ ---
27
+
28
+ Agent harnesses read a surprising amount of config: permission rules, hooks that execute
29
+ automatically, MCP servers that run third-party code, and memory files re-sent on every
30
+ request. Almost none of it is validated, and the failures are **silent**. A misspelled hook
31
+ event never fires. A deny rule naming a tool that does not exist blocks nothing — while
32
+ looking exactly like a guardrail. `Bash(*)` in an allow list means every command the model
33
+ proposes runs without asking you.
34
+
35
+ `agentdoctor` reads that config and tells you what is actually wrong with it:
36
+
37
+ ```sh
38
+ npx @jqntn/agentdoctor
39
+ ```
40
+
41
+ ```
42
+ agentdoctor scanned 8 config files in /work/api
43
+
44
+ .claude/settings.json
45
+ 4:7 error "Bash(*)" auto-approves every shell command, including ones you have not seen.
46
+ | Bash(*)
47
+ -> Replace the wildcard with the specific commands you actually want
48
+ unattended, e.g. "Bash(npm test:*)" or "Bash(git status)".
49
+ security/unrestricted-bash
50
+
51
+ 27:43 error PreToolUse hook uses curl | sh; the remote content is executed unreviewed on
52
+ every trigger.
53
+ | curl -sSL https://example.com/guard.sh | bash
54
+ -> Vendor the script into the repo and run it from a pinned path.
55
+ security/hook-remote-code
56
+
57
+ 35:21 error "PostToolUsee" is not a hook event. Did you mean "PostToolUse"? As written,
58
+ this hook never runs.
59
+ correctness/unknown-hook-event
60
+
61
+ CLAUDE.md
62
+ 1 warn CLAUDE.md is ~14,200 tokens of always-on context, sent with every request
63
+ (~$25/month at 3,300 requests, assuming it stays prompt-cached).
64
+ cost/memory-file-too-large
65
+
66
+ Summary Grade F 3 errors, 1 warning - 72 rules in 41ms
67
+ ```
68
+
69
+ Zero dependencies. No network calls. Credential files are never opened. MIT — all of it.
70
+
71
+ ## What it checks
72
+
73
+ **72 rules across five categories.** Full reasoning for every rule:
74
+ [rule reference](docs/rules.md), or `agentdoctor --explain <rule-id>`.
75
+
76
+ ### Security (22 rules)
77
+
78
+ The config surface is an execution surface. Blanket `Bash(*)` allows; destructive commands
79
+ pre-approved without confirmation (`sudo`, `rm -rf`, force push, `terraform destroy`,
80
+ `DROP TABLE`, …); hooks piping remote content into a shell (`curl | sh`,
81
+ `eval "$(curl …)"`); live credentials committed in config (Anthropic, OpenAI, GitHub, AWS,
82
+ Slack, Stripe keys, JWTs, private keys — always reported **redacted**); MCP servers running
83
+ unpinned packages or carrying tokens in URLs; `bypassPermissions` committed to shared repos;
84
+ loader-hijacking env vars; world-writable config.
85
+
86
+ ### Correctness (26 rules)
87
+
88
+ Config that is silently ignored is worse than config that errors, because you believe it is
89
+ working. Invalid JSON (which voids the whole file, permission rules included); misspelled
90
+ settings keys, hook events, tool names, and models — each with a did-you-mean; deny rules
91
+ that block nothing (an **error**, because it is a guardrail that only looks like one);
92
+ malformed hooks and invalid matcher regexes; duplicate agent/skill names; MCP servers with no
93
+ way to start.
94
+
95
+ ### Cost (8 rules)
96
+
97
+ Memory files and MCP tool schemas ride along on every request. Token counts for every memory
98
+ file with an estimated monthly cost — the estimate assumes the file stays prompt-cached
99
+ (that is exactly the content that caches) and states its assumptions; the same instruction
100
+ duplicated across files; pasted code blocks that belong behind a file path; skill
101
+ descriptions too vague for the model to ever load them.
102
+
103
+ ### Hygiene (8 rules)
104
+
105
+ `settings.local.json` not gitignored; machine-specific absolute paths in committed config;
106
+ local settings silently shadowing project settings; empty skill/agent bodies; duplicate
107
+ keybindings.
108
+
109
+ ### Policy (8 rules)
110
+
111
+ Team standards, enforced mechanically across every repo. Commit an
112
+ `agentdoctor.policy.json` and these activate — no flag, no account:
113
+
114
+ ```sh
115
+ agentdoctor --init-policy
116
+ ```
117
+
118
+ `requiredDeny`, `forbiddenAllow`, `allowedMcpServers`, `requiredHooks`, `maxMemoryTokens`,
119
+ `forbiddenPermissionModes`, plus drift detection when local settings quietly widen the
120
+ committed permission set. [Policy guide](docs/policy.md).
121
+
122
+ ## Adopt it in one command
123
+
124
+ ```sh
125
+ npx @jqntn/agentdoctor --init-ci # GitHub Actions: SARIF annotations on PRs + error gate
126
+ npx @jqntn/agentdoctor --init-skill # Claude Code skill: findings -> fixes, automatically
127
+ npx @jqntn/agentdoctor --init-agents # AGENTS.md section: same loop for Codex, Cursor, Gemini CLI
128
+ npx @jqntn/agentdoctor --badge # README badge with your current grade
129
+ npx @jqntn/agentdoctor --share # paste-ready score card (rule ids + counts only)
130
+ ```
131
+
132
+ Every audit ends in a grade - `A+` down to `F`, formula stated in the docs. The badge and the
133
+ share card contain rule ids and counts only, never messages, paths, or snippets, so they are
134
+ safe to post from private repos.
135
+
136
+ [![agentdoctor: A+](https://img.shields.io/badge/agentdoctor-A%2B-34D399)](https://jqntn.github.io/agentdoctor/)
137
+
138
+ ## CI
139
+
140
+ ```yaml
141
+ - run: npx @jqntn/agentdoctor --no-user --sarif > agentdoctor.sarif
142
+ continue-on-error: true
143
+ - uses: github/codeql-action/upload-sarif@v3
144
+ with: { sarif_file: agentdoctor.sarif }
145
+ - run: npx @jqntn/agentdoctor --no-user --quiet # exit 1 on errors
146
+ ```
147
+
148
+ Findings annotate the PR diff via SARIF. Adopting on a repo with existing findings? Record
149
+ them once and fail only on new ones — baselines are anchored to content, not line numbers, so
150
+ unrelated edits never invalidate them:
151
+
152
+ ```sh
153
+ agentdoctor --write-baseline .agentdoctor-baseline.json # once
154
+ agentdoctor --baseline .agentdoctor-baseline.json # in CI
155
+ ```
156
+
157
+ [CI guide](docs/ci.md) · [Baselines](docs/baselines.md)
158
+
159
+ ## Built for agents, audited by agents
160
+
161
+ Every capability is non-interactive and machine-readable: `--json` (stable schema, shipped as
162
+ [JSON Schema](schemas/report.schema.json)), `--sarif`, `--explain`, `--list-rules --json`,
163
+ deterministic ordering, redacted secrets safe for model context, pipe-safe output. The docs
164
+ site serves `llms.txt` and raw markdown.
165
+
166
+ The repo is also a **Claude Code plugin**: it ships the
167
+ [config-audit skill](skills/config-audit/SKILL.md) (audit -> fix loop, with fix recipes) and
168
+ an `/agentdoctor:audit` command. Install it any of three ways:
169
+
170
+ ```
171
+ /plugin marketplace add jqntn/agentdoctor # in Claude Code, then: /plugin install agentdoctor
172
+ npx @jqntn/agentdoctor --init-skill # copies the skill into this project
173
+ cp -r node_modules/@jqntn/agentdoctor/skills/config-audit .claude/skills/ # manual
174
+ ```
175
+
176
+ [Agent guide](docs/agents.md)
177
+
178
+ ## What it will not do
179
+
180
+ - **Never reads credential files.** `.credentials.json`, `.netrc`, private keys are excluded
181
+ by path before anything opens them — asserted by tests.
182
+ - **No network calls.** No telemetry, no update checks, nothing leaves the machine.
183
+ - **No dependencies.** A tool that warns about supply-chain risk should not install one.
184
+ - **Never edits your config.** Findings say what to change and why; the change is yours.
185
+
186
+ [Architecture and design principles](docs/architecture.md) · [FAQ](docs/faq.md) ·
187
+ [Security policy](SECURITY.md)
188
+
189
+ ## Documentation
190
+
191
+ | | |
192
+ |---|---|
193
+ | [Getting started](docs/getting-started.md) | Install, first run, reading findings, exit codes |
194
+ | [Configuration](docs/configuration.md) | Every flag, suppression, disabling rules |
195
+ | [Rule reference](docs/rules.md) | All 72 rules with reasoning |
196
+ | [CI setup](docs/ci.md) | GitHub Actions, SARIF, exit-code gating |
197
+ | [Baselines](docs/baselines.md) | Adopting on an existing repo |
198
+ | [Team policy](docs/policy.md) | One standard across many repos |
199
+ | [Output formats](docs/output.md) | JSON, SARIF, terminal |
200
+ | [Programmatic API](docs/api.md) | `run()`, custom rules, embedding |
201
+ | [For agents](docs/agents.md) | The machine-readable contract + fix loop |
202
+ | [Architecture](docs/architecture.md) | How it works, design principles |
203
+ | [FAQ](docs/faq.md) | |
204
+
205
+ ## Contributing
206
+
207
+ False positives are treated as more severe than missed findings — if a rule fires on your
208
+ legitimate config, [that is a bug](.github/ISSUE_TEMPLATE/false-positive.yml). Adding a rule
209
+ takes one object and two tests: see [CONTRIBUTING.md](CONTRIBUTING.md). The suite enforces the
210
+ invariants (every rule tested, zero findings on the clean fixture, zero dependencies), so if
211
+ `npm test` is green, the PR is reviewable.
212
+
213
+ ## License
214
+
215
+ [MIT](LICENSE). All 72 rules, every output format, no accounts, no telemetry, no paid tier.
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { run, VERSION, allRules, CATEGORIES } from '../src/index.js';
5
+ import { fingerprint } from '../src/engine.js';
6
+ import { renderTerminal, shouldUseColor } from '../src/report/terminal.js';
7
+ import { renderJson } from '../src/report/json.js';
8
+ import { renderSarif } from '../src/report/sarif.js';
9
+ import { initCi, initSkill, initAgents, shareCard, badgeMarkdown } from '../src/adopt.js';
10
+
11
+ const HELP = `agentdoctor ${VERSION} - lint your AI coding-agent configuration
12
+
13
+ Usage
14
+ agentdoctor [path] Audit a project (defaults to the current directory)
15
+ agentdoctor --explain <rule-id> Show what a rule checks and why
16
+ agentdoctor --list-rules List every rule
17
+
18
+ Output
19
+ --json Machine-readable findings on stdout
20
+ --sarif SARIF 2.1.0, for GitHub code scanning and other CI
21
+ --quiet Print nothing; rely on the exit code
22
+ --no-color Disable ANSI colour (also honours NO_COLOR)
23
+
24
+ Scope
25
+ --no-user Skip user-level config in ~/.claude
26
+ --only <cat,...> Run only these categories (${CATEGORIES.join(', ')})
27
+ --disable <id,...> Skip specific rules or whole categories
28
+ --min-severity <level> error | warning | info (default: info)
29
+
30
+ CI
31
+ --max-warnings <n> Fail if more than n warnings are found
32
+ --baseline <file> Ignore findings recorded in this file
33
+ --write-baseline <file> Record current findings as the accepted baseline
34
+
35
+ Team policy
36
+ --policy <file> Team policy file (default: agentdoctor.policy.json)
37
+ --init-policy Write a starter agentdoctor.policy.json
38
+
39
+ Adopt & share
40
+ --init-ci Write a ready-made GitHub Actions workflow (SARIF + gate)
41
+ --init-skill Write a Claude Code skill that audits and fixes config
42
+ --init-agents Add an audit section to AGENTS.md (Codex, Cursor, Gemini CLI,
43
+ and every other tool that reads it)
44
+ --badge Print a README badge showing this project's current grade
45
+ --share Print a paste-ready score card (rule ids and counts only,
46
+ never messages or paths - safe to share from private repos)
47
+
48
+ Exit codes
49
+ 0 no errors
50
+ 1 at least one error (or warnings over --max-warnings)
51
+ 2 bad usage
52
+
53
+ Suppress a rule for one file by adding a comment: agentdoctor-disable <rule-id>
54
+ `;
55
+
56
+ function parseArgs(argv) {
57
+ const flags = {
58
+ positional: [],
59
+ json: false, sarif: false, quiet: false, color: null,
60
+ noUser: false, only: [], disable: [], minSeverity: 'info',
61
+ maxWarnings: null, baseline: null, writeBaseline: null,
62
+ policy: null, explain: null, listRules: false,
63
+ initPolicy: false, initCi: false, initSkill: false, initAgents: false,
64
+ share: false, badge: false, help: false, version: false,
65
+ };
66
+ for (let i = 0; i < argv.length; i += 1) {
67
+ const arg = argv[i];
68
+ const next = () => {
69
+ const value = argv[i + 1];
70
+ if (value === undefined || value.startsWith('--')) {
71
+ throw new UsageError(`${arg} requires a value`);
72
+ }
73
+ i += 1;
74
+ return value;
75
+ };
76
+ switch (arg) {
77
+ case '--json': flags.json = true; break;
78
+ case '--sarif': flags.sarif = true; break;
79
+ case '--quiet': case '-q': flags.quiet = true; break;
80
+ case '--no-color': flags.color = false; break;
81
+ case '--color': flags.color = true; break;
82
+ case '--no-user': flags.noUser = true; break;
83
+ case '--only': flags.only.push(...next().split(',').map((s) => s.trim()).filter(Boolean)); break;
84
+ case '--disable': flags.disable.push(...next().split(',').map((s) => s.trim()).filter(Boolean)); break;
85
+ case '--min-severity': flags.minSeverity = next(); break;
86
+ case '--max-warnings': flags.maxWarnings = Number(next()); break;
87
+ case '--baseline': flags.baseline = next(); break;
88
+ case '--write-baseline': flags.writeBaseline = next(); break;
89
+ case '--policy': flags.policy = next(); break;
90
+ case '--explain': flags.explain = next(); break;
91
+ case '--list-rules': flags.listRules = true; break;
92
+ case '--init-policy': flags.initPolicy = true; break;
93
+ case '--init-ci': flags.initCi = true; break;
94
+ case '--init-skill': flags.initSkill = true; break;
95
+ case '--init-agents': flags.initAgents = true; break;
96
+ case '--share': flags.share = true; break;
97
+ case '--badge': flags.badge = true; break;
98
+ case '--help': case '-h': flags.help = true; break;
99
+ case '--version': case '-v': flags.version = true; break;
100
+ default:
101
+ if (arg.startsWith('-')) throw new UsageError(`Unknown option "${arg}"`);
102
+ flags.positional.push(arg);
103
+ }
104
+ }
105
+ return flags;
106
+ }
107
+
108
+ class UsageError extends Error {}
109
+
110
+ /**
111
+ * Piping into `head`, `less` or a closed pager is ordinary usage. Without this,
112
+ * the closed pipe surfaces as an unhandled EPIPE and a Node stack trace.
113
+ */
114
+ function ignoreBrokenPipe() {
115
+ for (const stream of [process.stdout, process.stderr]) {
116
+ stream.on('error', (error) => {
117
+ if (error.code === 'EPIPE') process.exit(0);
118
+ throw error;
119
+ });
120
+ }
121
+ }
122
+
123
+ const STARTER_POLICY = {
124
+ $comment: 'agentdoctor team policy. Commit this next to your repo root.',
125
+ $wildcards: 'A single * is literal. Use ** to mean "anything here".',
126
+ requiredDeny: ['Read(./.env*)', 'Read(**/.ssh/**)', 'Read(**/*.pem)', 'Read(**/.aws/credentials)'],
127
+ forbiddenAllow: ['Bash(*)', 'Bash(:*)', 'Bash()', 'WebFetch(*)', 'Bash(**sudo**)', 'Bash(**rm -rf**)'],
128
+ forbiddenPermissionModes: ['bypassPermissions'],
129
+ allowedMcpServers: [],
130
+ requiredHooks: [],
131
+ maxMemoryTokens: 6000,
132
+ };
133
+
134
+ function main() {
135
+ let flags;
136
+ try {
137
+ flags = parseArgs(process.argv.slice(2));
138
+ } catch (error) {
139
+ process.stderr.write(`${error.message}\n\nRun agentdoctor --help\n`);
140
+ return 2;
141
+ }
142
+
143
+ if (flags.help) {
144
+ process.stdout.write(HELP);
145
+ return 0;
146
+ }
147
+ if (flags.version) {
148
+ process.stdout.write(`${VERSION}\n`);
149
+ return 0;
150
+ }
151
+ if (flags.listRules) return listRules(flags);
152
+ if (flags.explain) return explainRule(flags.explain);
153
+
154
+ const root = resolve(flags.positional[0] ?? process.cwd());
155
+ if (!existsSync(root)) {
156
+ process.stderr.write(`No such directory: ${root}\n`);
157
+ return 2;
158
+ }
159
+
160
+ if (flags.initPolicy) return initPolicy(root);
161
+ if (flags.initCi || flags.initSkill || flags.initAgents) {
162
+ let failed = false;
163
+ for (const [enabled, init, next] of [
164
+ [flags.initCi, initCi, 'Findings will annotate PRs and errors will fail the build on the next push.'],
165
+ [flags.initSkill, initSkill, 'Claude Code will now offer config audits; try asking it to "audit my agent config".'],
166
+ [flags.initAgents, initAgents, 'Codex, Cursor, Gemini CLI and other AGENTS.md readers will now audit config after editing it.'],
167
+ ]) {
168
+ if (!enabled) continue;
169
+ const outcome = init(root);
170
+ process[outcome.written ? 'stdout' : 'stderr'].write(`${outcome.message}\n`);
171
+ if (outcome.written) process.stdout.write(`${next}\n`);
172
+ else failed = true;
173
+ }
174
+ return failed ? 2 : 0;
175
+ }
176
+
177
+ if (!['error', 'warning', 'info'].includes(flags.minSeverity)) {
178
+ process.stderr.write(`--min-severity must be error, warning or info\n`);
179
+ return 2;
180
+ }
181
+
182
+ let baseline = new Set();
183
+ if (flags.baseline) {
184
+ if (!existsSync(flags.baseline)) {
185
+ process.stderr.write(`Baseline file not found: ${flags.baseline}\n`);
186
+ return 2;
187
+ }
188
+ try {
189
+ const parsed = JSON.parse(readFileSync(flags.baseline, 'utf8'));
190
+ baseline = new Set(Array.isArray(parsed.fingerprints) ? parsed.fingerprints : []);
191
+ } catch (error) {
192
+ process.stderr.write(`Baseline file is not readable: ${error.message}\n`);
193
+ return 2;
194
+ }
195
+ }
196
+
197
+ const result = run(root, {
198
+ includeUserScope: !flags.noUser,
199
+ policyPath: flags.policy,
200
+ disabled: flags.disable,
201
+ minSeverity: flags.minSeverity,
202
+ only: flags.only,
203
+ baseline,
204
+ });
205
+
206
+ if (flags.writeBaseline) {
207
+ const payload = {
208
+ version: 1,
209
+ generatedBy: `agentdoctor ${VERSION}`,
210
+ fingerprints: result.findings.map((f) => fingerprint(f)),
211
+ };
212
+ writeFileSync(flags.writeBaseline, `${JSON.stringify(payload, null, 2)}\n`);
213
+ if (!flags.quiet) {
214
+ process.stdout.write(`Wrote ${payload.fingerprints.length} accepted findings to ${flags.writeBaseline}\n`);
215
+ }
216
+ return 0;
217
+ }
218
+
219
+ const payload = { ...result, version: VERSION };
220
+ if (flags.share) {
221
+ process.stdout.write(shareCard(result));
222
+ return 0;
223
+ }
224
+ if (flags.badge) {
225
+ process.stdout.write(badgeMarkdown(result));
226
+ return 0;
227
+ }
228
+ if (flags.json) {
229
+ process.stdout.write(`${renderJson(payload)}\n`);
230
+ } else if (flags.sarif) {
231
+ process.stdout.write(`${renderSarif(payload)}\n`);
232
+ } else if (!flags.quiet) {
233
+ const color = flags.color ?? shouldUseColor(process.stdout, process.env);
234
+ process.stdout.write(`${renderTerminal({ ...payload, color })}\n`);
235
+ }
236
+
237
+ const errors = result.findings.filter((f) => f.severity === 'error').length;
238
+ const warnings = result.findings.filter((f) => f.severity === 'warning').length;
239
+ if (errors > 0) return 1;
240
+ if (flags.maxWarnings !== null && Number.isFinite(flags.maxWarnings) && warnings > flags.maxWarnings) return 1;
241
+ return 0;
242
+ }
243
+
244
+ function listRules(flags) {
245
+ const wanted = flags.only.length ? new Set(flags.only) : null;
246
+ const rows = allRules
247
+ .filter((rule) => !wanted || wanted.has(rule.category) || wanted.has(rule.id))
248
+ .map((rule) => ({
249
+ id: rule.id,
250
+ severity: rule.severity,
251
+ title: rule.title,
252
+ }));
253
+ if (flags.json) {
254
+ process.stdout.write(`${JSON.stringify(rows, null, 2)}\n`);
255
+ return 0;
256
+ }
257
+ const width = Math.max(...rows.map((r) => r.id.length));
258
+ for (const row of rows) {
259
+ process.stdout.write(`${row.id.padEnd(width)} ${row.severity.padEnd(7)} ${row.title}\n`);
260
+ }
261
+ process.stdout.write(`\n${rows.length} rules\n`);
262
+ return 0;
263
+ }
264
+
265
+ function explainRule(id) {
266
+ const rule = allRules.find((r) => r.id === id);
267
+ if (!rule) {
268
+ const near = allRules.filter((r) => r.id.includes(id) || r.category === id).map((r) => r.id);
269
+ process.stderr.write(`No rule "${id}".${near.length ? `\n\nDid you mean:\n ${near.join('\n ')}\n` : '\n'}`);
270
+ return 2;
271
+ }
272
+ process.stdout.write([
273
+ rule.id,
274
+ '',
275
+ ` Title ${rule.title}`,
276
+ ` Category ${rule.category}`,
277
+ ` Severity ${rule.severity}`,
278
+ '',
279
+ ' Why it matters',
280
+ ...wrap(rule.help ?? '', 72).map((line) => ` ${line}`),
281
+ '',
282
+ ` Suppress with a comment in the offending file:`,
283
+ ` agentdoctor-disable ${rule.id}`,
284
+ '',
285
+ ].join('\n'));
286
+ return 0;
287
+ }
288
+
289
+ function initPolicy(root) {
290
+ const target = resolve(root, 'agentdoctor.policy.json');
291
+ if (existsSync(target)) {
292
+ process.stderr.write(`${target} already exists; not overwriting.\n`);
293
+ return 2;
294
+ }
295
+ writeFileSync(target, `${JSON.stringify(STARTER_POLICY, null, 2)}\n`);
296
+ process.stdout.write(`Wrote ${target}\nEdit it, commit it, then run agentdoctor in CI.\n`);
297
+ return 0;
298
+ }
299
+
300
+ function wrap(text, width) {
301
+ const words = String(text).split(/\s+/).filter(Boolean);
302
+ const lines = [];
303
+ let current = '';
304
+ for (const word of words) {
305
+ if (current === '') current = word;
306
+ else if (current.length + 1 + word.length > width) { lines.push(current); current = word; }
307
+ else current += ` ${word}`;
308
+ }
309
+ if (current) lines.push(current);
310
+ return lines.length ? lines : [''];
311
+ }
312
+
313
+ ignoreBrokenPipe();
314
+ process.exitCode = main();
package/docs/agents.md ADDED
@@ -0,0 +1,119 @@
1
+ # Using agentdoctor with AI agents
2
+
3
+ agentdoctor is built to be operated *by* agents, not just to audit their config. Every
4
+ capability is reachable non-interactively, every output has a machine-readable form, and every
5
+ finding carries enough context to act on without a human in the loop.
6
+
7
+ ## The contract, in one table
8
+
9
+ | Need | Command | Output |
10
+ |---|---|---|
11
+ | Audit a project | `agentdoctor <path> --no-user --json` | Findings JSON ([shape](output.md)) |
12
+ | Gate a change | `agentdoctor <path> --no-user --quiet` | Exit code only: 0 clean, 1 errors, 2 usage |
13
+ | Understand a rule | `agentdoctor --explain <rule-id>` | Rationale + suppression syntax, plain text |
14
+ | Enumerate rules | `agentdoctor --list-rules --json` | `[{ id, severity, title }]` |
15
+ | Accept a backlog | `agentdoctor --write-baseline <file>` | Fingerprint list, plain JSON |
16
+ | CI annotations | `agentdoctor --sarif` | SARIF 2.1.0 |
17
+
18
+ Guarantees an agent can rely on:
19
+
20
+ - **Deterministic**: same input tree → same findings, same order (severity, file, line).
21
+ - **Non-interactive**: no prompts, no TTY requirements, no network. Safe in any sandbox.
22
+ - **Stable JSON**: shape version `1` only ever gains fields. A JSON Schema ships in the
23
+ package at `schemas/report.schema.json`.
24
+ - **Redaction**: detected secrets never appear unredacted in any output format, so findings
25
+ are safe to pass through logs and model context.
26
+ - **Pipe-safe**: output through `head`, `less`, or a closed pipe exits cleanly.
27
+
28
+ ## The fix loop
29
+
30
+ The intended agentic workflow:
31
+
32
+ ```
33
+ 1. agentdoctor . --no-user --json -> findings
34
+ 2. for each finding (already sorted most-severe-first):
35
+ read finding.help -> the fix, in prose
36
+ edit finding.file at finding.line -> configPath names the exact key
37
+ 3. agentdoctor . --no-user --quiet -> exit 0 = verified fixed
38
+ ```
39
+
40
+ `configPath` (e.g. `permissions.allow[2]`) plus `snippet` (the offending value) identify the
41
+ exact edit target without re-parsing the report message. If a finding is intentional, write
42
+ the file-scoped suppression comment instead of deleting the rule:
43
+ `agentdoctor-disable <rule-id>` ([syntax](configuration.md)).
44
+
45
+ ## Adopting it for a user, in three commands
46
+
47
+ When an agent decides this tool belongs in a project (for example after an audit surfaces
48
+ findings), the full adoption is three non-interactive, idempotent-safe commands:
49
+
50
+ ```sh
51
+ npx @jqntn/agentdoctor --init-ci # writes .github/workflows/agentdoctor.yml (SARIF + gate)
52
+ npx @jqntn/agentdoctor --init-skill # writes .claude/skills/config-audit/SKILL.md
53
+ npx @jqntn/agentdoctor --write-baseline .agentdoctor-baseline.json # only if findings exist today
54
+ ```
55
+
56
+ Each `--init-*` command writes exactly one well-known file, refuses to overwrite, and exits 2
57
+ if the file already exists - so re-running is always safe. `--badge` prints README markdown
58
+ showing the current grade if the user wants it displayed.
59
+
60
+ ## Works with every coding agent
61
+
62
+ The CLI contract above is vendor-neutral - plain commands, JSON out, exit codes - so any
63
+ agent that can run a shell command can use agentdoctor. What differs per tool is where the
64
+ *instructions* live:
65
+
66
+ | Agent | Mechanism | Install |
67
+ |---|---|---|
68
+ | Claude Code | Skill + plugin (`/agentdoctor:audit`) | `/plugin marketplace add jqntn/agentdoctor` or `npx @jqntn/agentdoctor --init-skill` |
69
+ | OpenAI Codex | `AGENTS.md` | `npx @jqntn/agentdoctor --init-agents` |
70
+ | Cursor | `AGENTS.md` | `npx @jqntn/agentdoctor --init-agents` |
71
+ | Gemini CLI / Jules | `AGENTS.md` | `npx @jqntn/agentdoctor --init-agents` |
72
+ | Anything else | `AGENTS.md`, or just the CLI contract | `npx @jqntn/agentdoctor --init-agents` |
73
+
74
+ `--init-agents` writes a short marked section (`<!-- agentdoctor:start -->` ...
75
+ `<!-- agentdoctor:end -->`) into `AGENTS.md` - creating the file if absent, appending if
76
+ present, refusing if the section already exists - telling the agent to audit after any config
77
+ edit and how to run the fix loop. It is deliberately ~15 lines: AGENTS.md is always-on
78
+ context for these tools, and bloating it is exactly what agentdoctor's cost rules exist to
79
+ prevent.
80
+
81
+ Note the skill and the AGENTS.md section install *instructions*, not the binary: both invoke
82
+ `npx @jqntn/agentdoctor`, which prefers a project-local install and otherwise fetches on demand. Pin
83
+ it permanently with `npm install -D @jqntn/agentdoctor`.
84
+
85
+ For Codex specifically, a reusable custom prompt is one copy away (user-scope, so it works
86
+ across projects):
87
+
88
+ ```sh
89
+ mkdir -p ~/.codex/prompts
90
+ npx @jqntn/agentdoctor --init-agents # project instructions
91
+ cp node_modules/@jqntn/agentdoctor/skills/config-audit/SKILL.md ~/.codex/prompts/audit-config.md # optional /audit-config
92
+ ```
93
+
94
+ ## The standalone skill and plugin
95
+
96
+ The canonical skill lives at [`skills/config-audit/`](https://github.com/jqntn/agentdoctor/tree/main/skills/config-audit)
97
+ in the repo and inside the npm package. It contains the audit -> fix workflow plus
98
+ `references/fix-recipes.md` with per-rule fix patterns, and its `description` frontmatter is
99
+ written to trigger on config-audit requests, edits to `.claude/` files, and "my hook isn't
100
+ firing" symptoms.
101
+
102
+ Three ways to install it:
103
+
104
+ | Method | Command | Scope |
105
+ |---|---|---|
106
+ | Claude Code plugin | `/plugin marketplace add jqntn/agentdoctor` then `/plugin install agentdoctor` | everywhere (also adds `/agentdoctor:audit`) |
107
+ | CLI | `npx @jqntn/agentdoctor --init-skill` | this project |
108
+ | Manual | `cp -r node_modules/@jqntn/agentdoctor/skills/config-audit .claude/skills/` | anywhere |
109
+
110
+ All three install the same files - `--init-skill` copies them out of the package, so the
111
+ installed skill cannot drift from the published one (test-enforced).
112
+
113
+ ## For agents working on this repository
114
+
115
+ The repo root carries an `AGENTS.md` (mirrored by `CLAUDE.md`) with the build/test commands,
116
+ the architectural invariants, and the rules for adding rules. The docs site serves
117
+ [`llms.txt`](https://jqntn.github.io/agentdoctor/llms.txt) and a concatenated
118
+ `llms-full.txt`, and every docs page is also available as raw markdown at the same URL with
119
+ `.md` — agents should prefer those over scraping HTML.