@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/output.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Output formats
|
|
2
|
+
|
|
3
|
+
Three formats, one flag apart. The terminal report is for humans; `--json` is the stable
|
|
4
|
+
contract for scripts and agents; `--sarif` is for CI annotation.
|
|
5
|
+
|
|
6
|
+
## `--json`
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
agentdoctor --no-user --json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"version": 1,
|
|
15
|
+
"tool": "agentdoctor",
|
|
16
|
+
"toolVersion": "0.1.0",
|
|
17
|
+
"root": "/work/api",
|
|
18
|
+
"scannedFiles": [
|
|
19
|
+
{ "path": ".claude/settings.json", "kind": "settings", "scope": "project", "bytes": 512 }
|
|
20
|
+
],
|
|
21
|
+
"skippedFiles": ["/home/u/.claude/.credentials.json"],
|
|
22
|
+
"rulesRun": ["correctness/invalid-json", "..."],
|
|
23
|
+
"suppressed": 0,
|
|
24
|
+
"grade": "D",
|
|
25
|
+
"summary": { "error": 2, "warning": 1, "info": 0 },
|
|
26
|
+
"findings": [
|
|
27
|
+
{
|
|
28
|
+
"ruleId": "security/unrestricted-bash",
|
|
29
|
+
"severity": "error",
|
|
30
|
+
"category": "security",
|
|
31
|
+
"message": "\"Bash(*)\" auto-approves every shell command, including ones you have not seen.",
|
|
32
|
+
"help": "Replace the wildcard with the specific commands you actually want unattended...",
|
|
33
|
+
"file": ".claude/settings.json",
|
|
34
|
+
"absolutePath": "/work/api/.claude/settings.json",
|
|
35
|
+
"line": 4,
|
|
36
|
+
"column": 7,
|
|
37
|
+
"configPath": "permissions.allow[0]",
|
|
38
|
+
"snippet": "Bash(*)"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Field notes:
|
|
45
|
+
|
|
46
|
+
- `grade` is the health grade, computed from the post-filter findings: `A+` zero findings,
|
|
47
|
+
`A` info only, `B` 1-2 warnings, `C` 3+ warnings, `D` 1-2 errors, `F` 3+ errors.
|
|
48
|
+
- `version` is the format version. Additions are the only change ever made to shape `1`;
|
|
49
|
+
removals or renames would bump it.
|
|
50
|
+
- `findings` is sorted: severity first (`error` > `warning` > `info`), then file, then line.
|
|
51
|
+
- `column`, `configPath`, `snippet`, and `help` are `null` when not applicable.
|
|
52
|
+
- `snippet` never contains an unredacted secret — credential-shaped values are truncated to
|
|
53
|
+
a prefix/suffix with `(redacted)`.
|
|
54
|
+
- `file` is display-relative (repo-relative, or `~/`-prefixed for user scope);
|
|
55
|
+
`absolutePath` is absolute.
|
|
56
|
+
- A machine-readable JSON Schema ships in the package: `schemas/report.schema.json`.
|
|
57
|
+
|
|
58
|
+
`--list-rules --json` emits the catalogue as `[{ id, severity, title }]`.
|
|
59
|
+
|
|
60
|
+
## `--sarif`
|
|
61
|
+
|
|
62
|
+
SARIF 2.1.0, consumable by GitHub code scanning and any SARIF-aware tool. Findings appear as
|
|
63
|
+
inline annotations on the PR diff.
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
agentdoctor --no-user --sarif > agentdoctor.sarif
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Properties worth knowing:
|
|
70
|
+
|
|
71
|
+
- Severities map `error → error`, `warning → warning`, `info → note`.
|
|
72
|
+
- `partialFingerprints.agentdoctorFingerprint` gives stable finding identity across runs, so
|
|
73
|
+
GitHub tracks findings as "existing" rather than re-announcing them per commit.
|
|
74
|
+
- Artifact URIs are repo-relative. Files outside the repo (user scope) are shortened to a
|
|
75
|
+
suffix rather than leaking an absolute home path into CI logs.
|
|
76
|
+
- Every rule referenced by a result includes its full description and help text in
|
|
77
|
+
`tool.driver.rules`, so the annotation is self-explanatory in the GitHub UI.
|
|
78
|
+
|
|
79
|
+
Wiring for GitHub Actions is in the [CI guide](ci.md).
|
|
80
|
+
|
|
81
|
+
## Terminal report
|
|
82
|
+
|
|
83
|
+
The default. Grouped by file so you fix one file at a time; within a file, sorted by severity
|
|
84
|
+
then line. Color respects `NO_COLOR`, `FORCE_COLOR`, and TTY detection, and degrades to plain
|
|
85
|
+
text in pipes. Every finding ends with its rule id so `--explain` is always one copy-paste
|
|
86
|
+
away.
|
|
87
|
+
|
|
88
|
+
The summary line always includes: the grade, counts by severity, rules run, elapsed time,
|
|
89
|
+
suppressed findings (baseline + inline), and how many credential files were skipped unread.
|
|
90
|
+
|
|
91
|
+
## Exit codes (all formats)
|
|
92
|
+
|
|
93
|
+
| Code | Meaning |
|
|
94
|
+
|---|---|
|
|
95
|
+
| 0 | No errors; warnings within `--max-warnings` if set |
|
|
96
|
+
| 1 | At least one error, or warnings over the limit |
|
|
97
|
+
| 2 | Usage error |
|
package/docs/policy.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Team policy
|
|
2
|
+
|
|
3
|
+
One repo can be audited by reading it. Forty repos need a written standard that CI checks
|
|
4
|
+
mechanically — that is what `agentdoctor.policy.json` is. Commit it at the repo root (or ship
|
|
5
|
+
the same file to every repo from a central location) and the eight `policy/*` rules activate
|
|
6
|
+
automatically. No flag, no account. Repos without a policy file never see these rules fire.
|
|
7
|
+
|
|
8
|
+
## Quick start
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
agentdoctor --init-policy
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
writes a starter policy:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"requiredDeny": ["Read(./.env*)", "Read(**/.ssh/**)", "Read(**/*.pem)", "Read(**/.aws/credentials)"],
|
|
19
|
+
"forbiddenAllow": ["Bash(*)", "Bash(:*)", "Bash()", "WebFetch(*)", "Bash(**sudo**)", "Bash(**rm -rf**)"],
|
|
20
|
+
"forbiddenPermissionModes": ["bypassPermissions"],
|
|
21
|
+
"allowedMcpServers": [],
|
|
22
|
+
"requiredHooks": [],
|
|
23
|
+
"maxMemoryTokens": 6000
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Edit, commit, done. `agentdoctor` now fails (exit 1) when the repo violates it.
|
|
28
|
+
|
|
29
|
+
## Fields
|
|
30
|
+
|
|
31
|
+
### `requiredDeny: string[]`
|
|
32
|
+
|
|
33
|
+
Deny rules every repo must carry, in any settings file. Enforced by
|
|
34
|
+
`policy/missing-required-deny`. Deny rules are the only guardrail evaluated before anything
|
|
35
|
+
runs, which makes them the one thing worth mandating centrally.
|
|
36
|
+
|
|
37
|
+
### `forbiddenAllow: string[]`
|
|
38
|
+
|
|
39
|
+
Allow rules no repo may carry. Enforced by `policy/forbidden-allow`.
|
|
40
|
+
|
|
41
|
+
### `allowedMcpServers: string[]`
|
|
42
|
+
|
|
43
|
+
If present, every configured MCP server name must match an entry. Enforced by
|
|
44
|
+
`policy/unapproved-mcp-server`. This turns "someone committed a new MCP server" from a silent
|
|
45
|
+
event into a review decision. Omit the field entirely to skip this check; an empty array means
|
|
46
|
+
*no servers are approved*.
|
|
47
|
+
|
|
48
|
+
### `requiredHooks: string[]`
|
|
49
|
+
|
|
50
|
+
Hook events that must be configured, e.g. `["PreToolUse"]` if your org mandates a guardrail
|
|
51
|
+
hook. Enforced by `policy/required-hook-missing`.
|
|
52
|
+
|
|
53
|
+
### `maxMemoryTokens: number`
|
|
54
|
+
|
|
55
|
+
A ceiling on the estimated token size of the project's always-on memory files (`CLAUDE.md`
|
|
56
|
+
et al., user scope excluded). Enforced by `policy/memory-budget-exceeded`. A context budget is
|
|
57
|
+
the only thing that stops memory files growing without limit.
|
|
58
|
+
|
|
59
|
+
### `forbiddenPermissionModes: string[]`
|
|
60
|
+
|
|
61
|
+
Usually `["bypassPermissions"]`. Enforced by `policy/forbidden-permission-mode`.
|
|
62
|
+
|
|
63
|
+
## Wildcard semantics
|
|
64
|
+
|
|
65
|
+
Permission rules themselves contain `*`, so policy patterns treat it literally:
|
|
66
|
+
|
|
67
|
+
- A single `*` is **literal**. `"Bash(*)"` forbids exactly the rule `Bash(*)` — it does **not**
|
|
68
|
+
forbid `Bash(npm test:*)`.
|
|
69
|
+
- `**` is the **wildcard**. `"Bash(**)"` matches every Bash rule; `"Bash(**sudo**)"` matches
|
|
70
|
+
any Bash rule mentioning sudo.
|
|
71
|
+
|
|
72
|
+
This is the difference between "nobody may have the blanket rule" and "nobody may run Bash at
|
|
73
|
+
all" — the starter policy uses both deliberately.
|
|
74
|
+
|
|
75
|
+
## Two rules that need no policy fields
|
|
76
|
+
|
|
77
|
+
- `policy/permission-drift` fires when `.claude/settings.local.json` adds an *unrestricted*
|
|
78
|
+
allow rule the committed project config does not grant. Local settings are invisible in code
|
|
79
|
+
review; this makes the widening visible.
|
|
80
|
+
- `policy/file-invalid` fires when the policy file itself fails to parse — a policy that
|
|
81
|
+
silently enforces nothing is the worst state for a guardrail.
|
|
82
|
+
|
|
83
|
+
## Rolling out across an organisation
|
|
84
|
+
|
|
85
|
+
1. Write one policy centrally. Start with `requiredDeny` + `forbiddenPermissionModes` only —
|
|
86
|
+
they are the least controversial and catch the worst failure modes.
|
|
87
|
+
2. Ship it to each repo (commit it, or `curl` it in CI before running agentdoctor).
|
|
88
|
+
3. Run `agentdoctor --no-user` in CI. Use a [baseline](baselines.md) per repo if there is a
|
|
89
|
+
backlog.
|
|
90
|
+
4. Tighten over time: add `allowedMcpServers` once you have inventoried what is in use, then
|
|
91
|
+
`maxMemoryTokens` once teams have trimmed.
|
|
92
|
+
|
|
93
|
+
A JSON Schema for the policy file ships with the package (`schemas/policy.schema.json`) and is
|
|
94
|
+
served on the docs site, so editors validate it as you type.
|
package/docs/rules.md
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# Rule reference
|
|
2
|
+
|
|
3
|
+
72 rules. `agentdoctor --explain <rule-id>` prints any of these from the CLI.
|
|
4
|
+
|
|
5
|
+
## Correctness
|
|
6
|
+
|
|
7
|
+
Config the harness is silently ignoring. These are the findings where you believe something is configured and it is not.
|
|
8
|
+
|
|
9
|
+
### `correctness/invalid-json`
|
|
10
|
+
|
|
11
|
+
**Config file is not valid JSON** · `error`
|
|
12
|
+
|
|
13
|
+
The harness cannot read this file, so every setting in it is silently ignored — including any permission rules you thought were protecting you.
|
|
14
|
+
|
|
15
|
+
### `correctness/unknown-settings-key`
|
|
16
|
+
|
|
17
|
+
**Unrecognised settings key** · `warning`
|
|
18
|
+
|
|
19
|
+
Unknown keys are ignored without warning, so a typo means the setting never applies.
|
|
20
|
+
|
|
21
|
+
### `correctness/unknown-permission-key`
|
|
22
|
+
|
|
23
|
+
**Unrecognised key under permissions** · `warning`
|
|
24
|
+
|
|
25
|
+
Valid keys are: allow, deny, ask, defaultMode, additionalDirectories, disableBypassPermissionsMode.
|
|
26
|
+
|
|
27
|
+
### `correctness/permissions-wrong-type`
|
|
28
|
+
|
|
29
|
+
**Permission bucket is not an array** · `error`
|
|
30
|
+
|
|
31
|
+
allow, deny and ask must each be an array of rule strings. A string or object here means the rules never load.
|
|
32
|
+
|
|
33
|
+
### `correctness/permission-unknown-tool`
|
|
34
|
+
|
|
35
|
+
**Permission rule names an unknown tool** · `warning`
|
|
36
|
+
|
|
37
|
+
Tool names are case-sensitive. A rule naming a tool that does not exist never matches anything, so a deny rule written this way protects nothing.
|
|
38
|
+
|
|
39
|
+
### `correctness/permission-non-string`
|
|
40
|
+
|
|
41
|
+
**Permission rule is not a string** · `error`
|
|
42
|
+
|
|
43
|
+
Each entry must be a string like "Bash(npm test:*)".
|
|
44
|
+
|
|
45
|
+
### `correctness/duplicate-permission`
|
|
46
|
+
|
|
47
|
+
**Duplicate permission rule** · `info`
|
|
48
|
+
|
|
49
|
+
Harmless, but usually a sign of a merge that went wrong or a rule that was meant to be edited rather than added.
|
|
50
|
+
|
|
51
|
+
### `correctness/allow-deny-conflict`
|
|
52
|
+
|
|
53
|
+
**Same rule in both allow and deny** · `warning`
|
|
54
|
+
|
|
55
|
+
Deny wins, so the allow entry is dead config. Remove it so the intent is unambiguous to the next reader.
|
|
56
|
+
|
|
57
|
+
### `correctness/unknown-hook-event`
|
|
58
|
+
|
|
59
|
+
**Unknown hook event** · `error`
|
|
60
|
+
|
|
61
|
+
Valid events: PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact, SessionStart, SessionEnd. Events are case-sensitive and a misspelled one never fires.
|
|
62
|
+
|
|
63
|
+
### `correctness/hook-malformed`
|
|
64
|
+
|
|
65
|
+
**Hook entry has the wrong shape** · `error`
|
|
66
|
+
|
|
67
|
+
Each event maps to an array of { matcher, hooks: [{ type: "command", command: "..." }] }. A near-miss shape is dropped silently.
|
|
68
|
+
|
|
69
|
+
### `correctness/hook-matcher-ignored`
|
|
70
|
+
|
|
71
|
+
**Matcher set on an event that has no tool** · `info`
|
|
72
|
+
|
|
73
|
+
Only PreToolUse, PostToolUse, PreCompact use a matcher. Elsewhere it is ignored, which can look like the hook is scoped when it is not.
|
|
74
|
+
|
|
75
|
+
### `correctness/hook-matcher-invalid-regex`
|
|
76
|
+
|
|
77
|
+
**Hook matcher is not a valid pattern** · `error`
|
|
78
|
+
|
|
79
|
+
Matchers are treated as regular expressions. An invalid pattern means the hook silently never matches.
|
|
80
|
+
|
|
81
|
+
### `correctness/hook-matcher-unknown-tool`
|
|
82
|
+
|
|
83
|
+
**Hook matcher names no existing tool** · `warning`
|
|
84
|
+
|
|
85
|
+
Check the spelling and casing of the tool name. A matcher that matches nothing is a hook that never fires.
|
|
86
|
+
|
|
87
|
+
### `correctness/invalid-model`
|
|
88
|
+
|
|
89
|
+
**Unrecognised model name** · `warning`
|
|
90
|
+
|
|
91
|
+
Use an alias (opus, sonnet, haiku) or a full model id. An unknown value falls back to the default without telling you.
|
|
92
|
+
|
|
93
|
+
### `correctness/agent-missing-frontmatter`
|
|
94
|
+
|
|
95
|
+
**Subagent definition has no frontmatter** · `error`
|
|
96
|
+
|
|
97
|
+
A subagent file needs a --- delimited frontmatter block with at least name and description. Without it the agent is not registered.
|
|
98
|
+
|
|
99
|
+
### `correctness/agent-missing-field`
|
|
100
|
+
|
|
101
|
+
**Subagent is missing a required field** · `error`
|
|
102
|
+
|
|
103
|
+
Both name and description are required. The description is what the orchestrating model reads to decide whether to delegate, so an empty one means the agent is never chosen.
|
|
104
|
+
|
|
105
|
+
### `correctness/agent-name-mismatch`
|
|
106
|
+
|
|
107
|
+
**Subagent name does not match its filename** · `warning`
|
|
108
|
+
|
|
109
|
+
Keep the frontmatter name and the filename in sync; mismatches make agents hard to find and, depending on the harness version, can shadow each other.
|
|
110
|
+
|
|
111
|
+
### `correctness/agent-unknown-tool`
|
|
112
|
+
|
|
113
|
+
**Subagent grants a tool that does not exist** · `warning`
|
|
114
|
+
|
|
115
|
+
Tool names in the tools list are case-sensitive. An unknown entry is dropped, so the agent quietly runs without the capability you meant to give it.
|
|
116
|
+
|
|
117
|
+
### `correctness/duplicate-agent-name`
|
|
118
|
+
|
|
119
|
+
**Two subagents share a name** · `error`
|
|
120
|
+
|
|
121
|
+
Names must be unique; the loser is unreachable. Project-scope agents shadow user-scope agents with the same name.
|
|
122
|
+
|
|
123
|
+
### `correctness/skill-name-mismatch`
|
|
124
|
+
|
|
125
|
+
**Skill name does not match its directory** · `error`
|
|
126
|
+
|
|
127
|
+
A skill is invoked by its directory name, so a mismatched frontmatter name makes the skill impossible to invoke by the name it advertises.
|
|
128
|
+
|
|
129
|
+
### `correctness/skill-missing-field`
|
|
130
|
+
|
|
131
|
+
**Skill is missing a required field** · `error`
|
|
132
|
+
|
|
133
|
+
name and description are both required. The description is the only thing the model sees when deciding whether to load the skill.
|
|
134
|
+
|
|
135
|
+
### `correctness/duplicate-skill-name`
|
|
136
|
+
|
|
137
|
+
**Two skills share a name** · `error`
|
|
138
|
+
|
|
139
|
+
Only one wins. Rename one, or move it under a directory-scoped path if the collision is deliberate.
|
|
140
|
+
|
|
141
|
+
### `correctness/mcp-server-incomplete`
|
|
142
|
+
|
|
143
|
+
**MCP server has no way to start** · `error`
|
|
144
|
+
|
|
145
|
+
A server needs either "command" (stdio) or "url" (SSE/HTTP). Without one the server fails to connect on every session start.
|
|
146
|
+
|
|
147
|
+
### `correctness/mcp-server-toggled-both-ways`
|
|
148
|
+
|
|
149
|
+
**MCP server both enabled and disabled** · `warning`
|
|
150
|
+
|
|
151
|
+
Remove it from one of the two lists so the intended state is obvious.
|
|
152
|
+
|
|
153
|
+
### `correctness/statusline-malformed`
|
|
154
|
+
|
|
155
|
+
**statusLine is misconfigured** · `warning`
|
|
156
|
+
|
|
157
|
+
statusLine must be an object with type "command" and a command string.
|
|
158
|
+
|
|
159
|
+
### `correctness/env-non-string-value`
|
|
160
|
+
|
|
161
|
+
**Environment value is not a string** · `warning`
|
|
162
|
+
|
|
163
|
+
Environment variables are strings. Numbers and booleans here may be dropped or coerced unpredictably — quote them.
|
|
164
|
+
|
|
165
|
+
## Security
|
|
166
|
+
|
|
167
|
+
The config surface is an execution surface. These rules find the places where it is wider than intended.
|
|
168
|
+
|
|
169
|
+
### `security/unrestricted-bash`
|
|
170
|
+
|
|
171
|
+
**Blanket Bash allow rule** · `error`
|
|
172
|
+
|
|
173
|
+
Replace the wildcard with the specific commands you actually want unattended, e.g. "Bash(npm test:*)" or "Bash(git status)". A blanket allow means any command the model proposes runs without asking you.
|
|
174
|
+
|
|
175
|
+
### `security/destructive-allow`
|
|
176
|
+
|
|
177
|
+
**Destructive command pre-approved** · `error`
|
|
178
|
+
|
|
179
|
+
Move this rule to permissions.ask so you still get a prompt, or narrow it to the safe subset of the command.
|
|
180
|
+
|
|
181
|
+
### `security/bypass-permissions-default`
|
|
182
|
+
|
|
183
|
+
**Permission checks disabled by default** · `error`
|
|
184
|
+
|
|
185
|
+
Use "default" or "acceptEdits" for day-to-day work and opt into bypass explicitly per session. Committing bypassPermissions applies it to everyone who checks out the repo.
|
|
186
|
+
|
|
187
|
+
### `security/hooks-globally-disabled`
|
|
188
|
+
|
|
189
|
+
**All hooks disabled** · `warning`
|
|
190
|
+
|
|
191
|
+
If hooks were disabled to work around one noisy hook, remove that hook instead. disableAllHooks also silences hooks your team relies on for guardrails.
|
|
192
|
+
|
|
193
|
+
### `security/hook-remote-code`
|
|
194
|
+
|
|
195
|
+
**Hook downloads and executes remote code** · `error`
|
|
196
|
+
|
|
197
|
+
Vendor the script into the repo and run it from a pinned path. Hooks run automatically with your full user privileges and no confirmation, so whoever controls that URL controls your machine.
|
|
198
|
+
|
|
199
|
+
### `security/hook-unpinned-path`
|
|
200
|
+
|
|
201
|
+
**Hook command resolves through PATH or cwd** · `warning`
|
|
202
|
+
|
|
203
|
+
Use an absolute path or "$CLAUDE_PROJECT_DIR/.claude/hooks/name.sh". A bare name resolves via PATH, so a same-named file earlier in PATH — or in a repo you clone — runs instead.
|
|
204
|
+
|
|
205
|
+
### `security/hook-dangerous-command`
|
|
206
|
+
|
|
207
|
+
**Hook runs a destructive command** · `warning`
|
|
208
|
+
|
|
209
|
+
Hooks fire automatically with no confirmation step. Anything irreversible belongs in a command you invoke deliberately, not in a hook.
|
|
210
|
+
|
|
211
|
+
### `security/secret-in-config`
|
|
212
|
+
|
|
213
|
+
**Credential hardcoded in agent config** · `error`
|
|
214
|
+
|
|
215
|
+
Move the value to a secret manager or an untracked env file and reference it indirectly. Config files are committed, synced and shared far more often than people expect.
|
|
216
|
+
|
|
217
|
+
### `security/dangerous-env-var`
|
|
218
|
+
|
|
219
|
+
**Loader-influencing environment variable set** · `warning`
|
|
220
|
+
|
|
221
|
+
Set these per-command instead of session-wide. Anything defined in settings.env applies to every process the agent spawns for the whole session.
|
|
222
|
+
|
|
223
|
+
### `security/broad-additional-directory`
|
|
224
|
+
|
|
225
|
+
**Filesystem root granted as a working directory** · `error`
|
|
226
|
+
|
|
227
|
+
List only the specific sibling directories the agent needs. Granting "/" or your home directory hands it every SSH key, browser profile and other project on the machine.
|
|
228
|
+
|
|
229
|
+
### `security/unrestricted-egress`
|
|
230
|
+
|
|
231
|
+
**Unrestricted network egress pre-approved** · `warning`
|
|
232
|
+
|
|
233
|
+
Scope WebFetch to the domains you actually need, e.g. "WebFetch(domain:docs.example.com)". An open fetch rule is a one-step path for anything in your context to leave the machine.
|
|
234
|
+
|
|
235
|
+
### `security/sensitive-read-allowed`
|
|
236
|
+
|
|
237
|
+
**Credential file explicitly readable** · `error`
|
|
238
|
+
|
|
239
|
+
Remove the rule and add the path to permissions.deny instead. Secrets read into context end up in transcripts, logs and any tool call the model makes next.
|
|
240
|
+
|
|
241
|
+
### `security/missing-secret-denies`
|
|
242
|
+
|
|
243
|
+
**No deny rules protecting secrets** · `info`
|
|
244
|
+
|
|
245
|
+
Add a deny list such as ["Read(./.env*)", "Read(**/.ssh/**)", "Read(**/*.pem)", "Read(**/.aws/credentials)"]. Deny rules are the only guardrail that survives an accepted prompt, since they are checked before anything runs.
|
|
246
|
+
|
|
247
|
+
### `security/mcp-unpinned-package`
|
|
248
|
+
|
|
249
|
+
**MCP server runs an unpinned remote package** · `warning`
|
|
250
|
+
|
|
251
|
+
Pin the exact version, e.g. "@scope/server@1.4.2". With "@latest" or no version, every session silently installs whatever was published most recently, including a compromised release.
|
|
252
|
+
|
|
253
|
+
### `security/mcp-auto-enable-all`
|
|
254
|
+
|
|
255
|
+
**Project MCP servers auto-enabled without review** · `warning`
|
|
256
|
+
|
|
257
|
+
Leave this off and enable servers explicitly via enabledMcpjsonServers. Otherwise cloning a repo is enough to run its MCP servers on your machine.
|
|
258
|
+
|
|
259
|
+
### `security/mcp-plaintext-url-credential`
|
|
260
|
+
|
|
261
|
+
**Credential embedded in MCP server URL** · `error`
|
|
262
|
+
|
|
263
|
+
Move the token into a header sourced from the environment. URLs land in logs, crash reports and shell history.
|
|
264
|
+
|
|
265
|
+
### `security/world-writable-config`
|
|
266
|
+
|
|
267
|
+
**Agent config writable by other users** · `error`
|
|
268
|
+
|
|
269
|
+
Run "chmod go-w" on the file. Any user who can write your agent config can add a hook, and hooks execute automatically as you.
|
|
270
|
+
|
|
271
|
+
### `security/hook-script-not-executable`
|
|
272
|
+
|
|
273
|
+
**Hook script is world-writable or missing** · `warning`
|
|
274
|
+
|
|
275
|
+
Keep hook scripts inside the repo, owned by you, and not group-writable.
|
|
276
|
+
|
|
277
|
+
### `security/apikeyhelper-inline-secret`
|
|
278
|
+
|
|
279
|
+
**apiKeyHelper echoes a literal key** · `error`
|
|
280
|
+
|
|
281
|
+
Point apiKeyHelper at a script that reads from your OS keychain or secret manager, rather than embedding the key in the command.
|
|
282
|
+
|
|
283
|
+
### `security/deny-bucket-empty-with-broad-allow`
|
|
284
|
+
|
|
285
|
+
**Broad allow list with no deny list** · `warning`
|
|
286
|
+
|
|
287
|
+
Pair permissive allow rules with explicit denies. Deny is evaluated first and is the only rule class the model cannot talk its way past.
|
|
288
|
+
|
|
289
|
+
### `security/bypass-mode-not-locked`
|
|
290
|
+
|
|
291
|
+
**Bypass mode not disabled for the project** · `info`
|
|
292
|
+
|
|
293
|
+
Set permissions.disableBypassPermissionsMode to "disable" in committed project settings to stop anyone opting out of prompts in this repo.
|
|
294
|
+
|
|
295
|
+
### `security/invalid-permission-mode`
|
|
296
|
+
|
|
297
|
+
**Unknown permission mode** · `error`
|
|
298
|
+
|
|
299
|
+
Use one of: default, acceptEdits, plan, bypassPermissions. An unrecognised mode is ignored, so you silently fall back to the default.
|
|
300
|
+
|
|
301
|
+
## Cost
|
|
302
|
+
|
|
303
|
+
Memory files and tool schemas are re-sent on every request, so their size is a recurring charge. These rules quantify it.
|
|
304
|
+
|
|
305
|
+
### `cost/memory-file-too-large`
|
|
306
|
+
|
|
307
|
+
**Memory file is large enough to cost real money** · `warning`
|
|
308
|
+
|
|
309
|
+
Move reference material into a skill or a linked file that gets read on demand. Memory files are prepended to every request, so their size multiplies by every turn you take.
|
|
310
|
+
|
|
311
|
+
### `cost/total-memory-budget`
|
|
312
|
+
|
|
313
|
+
**Combined always-on context is heavy** · `warning`
|
|
314
|
+
|
|
315
|
+
Aim to keep the always-loaded total under a few thousand tokens. Everything here competes with the actual task for the model attention you are paying for.
|
|
316
|
+
|
|
317
|
+
### `cost/duplicated-memory-instructions`
|
|
318
|
+
|
|
319
|
+
**The same instruction appears in several memory files** · `info`
|
|
320
|
+
|
|
321
|
+
Keep each instruction in exactly one file. Duplicates cost tokens twice and, worse, drift apart until they contradict each other.
|
|
322
|
+
|
|
323
|
+
### `cost/many-mcp-servers`
|
|
324
|
+
|
|
325
|
+
**Many MCP servers enabled at once** · `warning`
|
|
326
|
+
|
|
327
|
+
Enable servers per project rather than globally. Every connected server contributes its tool schemas to the context window on every request, whether or not you use it.
|
|
328
|
+
|
|
329
|
+
### `cost/vague-skill-description`
|
|
330
|
+
|
|
331
|
+
**Skill description gives the model nothing to match on** · `warning`
|
|
332
|
+
|
|
333
|
+
Write descriptions as trigger conditions: "Use when the user asks to X, mentions Y, or is working on Z." The description is the only signal the model has, so a vague one means the skill you wrote is never used.
|
|
334
|
+
|
|
335
|
+
### `cost/vague-agent-description`
|
|
336
|
+
|
|
337
|
+
**Subagent description will not attract delegation** · `info`
|
|
338
|
+
|
|
339
|
+
State what the agent is for and when to pick it. Orchestrators route on this string alone.
|
|
340
|
+
|
|
341
|
+
### `cost/memory-contains-generated-content`
|
|
342
|
+
|
|
343
|
+
**Memory file contains content that belongs in a file, not in context** · `warning`
|
|
344
|
+
|
|
345
|
+
Reference the file by path instead of pasting it. The agent can read a path in one tool call; pasted content is paid for on every single request forever.
|
|
346
|
+
|
|
347
|
+
### `cost/no-cleanup-period`
|
|
348
|
+
|
|
349
|
+
**Transcript retention never trimmed** · `info`
|
|
350
|
+
|
|
351
|
+
Set cleanupPeriodDays to something like 30. Old transcripts are dead weight on disk and, if they contain customer data, a growing liability.
|
|
352
|
+
|
|
353
|
+
## Hygiene
|
|
354
|
+
|
|
355
|
+
Legal, safe config that will still cause avoidable confusion or leak personal settings between machines.
|
|
356
|
+
|
|
357
|
+
### `hygiene/local-settings-not-ignored`
|
|
358
|
+
|
|
359
|
+
**Local settings file is not gitignored** · `error`
|
|
360
|
+
|
|
361
|
+
Add ".claude/settings.local.json" to .gitignore. That file is where personal overrides and machine-specific paths go, and committing it pushes your permissions onto everyone else.
|
|
362
|
+
|
|
363
|
+
### `hygiene/empty-config`
|
|
364
|
+
|
|
365
|
+
**Config file has no effective content** · `info`
|
|
366
|
+
|
|
367
|
+
Delete it, or fill it in. An empty file reads as "configured" to the next person who opens the repo.
|
|
368
|
+
|
|
369
|
+
### `hygiene/skill-body-empty`
|
|
370
|
+
|
|
371
|
+
**Skill has frontmatter but no instructions** · `warning`
|
|
372
|
+
|
|
373
|
+
The body is what the model actually follows once the skill loads. Frontmatter alone advertises a capability that does nothing.
|
|
374
|
+
|
|
375
|
+
### `hygiene/agent-body-empty`
|
|
376
|
+
|
|
377
|
+
**Subagent has no system prompt** · `warning`
|
|
378
|
+
|
|
379
|
+
The body of an agent file is its system prompt. Without one the subagent behaves like a default agent with a narrower toolset.
|
|
380
|
+
|
|
381
|
+
### `hygiene/no-project-memory`
|
|
382
|
+
|
|
383
|
+
**No project memory file** · `info`
|
|
384
|
+
|
|
385
|
+
A short CLAUDE.md covering build/test commands and project conventions removes the same handful of questions from every session.
|
|
386
|
+
|
|
387
|
+
### `hygiene/absolute-home-path`
|
|
388
|
+
|
|
389
|
+
**Committed config contains a machine-specific path** · `warning`
|
|
390
|
+
|
|
391
|
+
Use $CLAUDE_PROJECT_DIR or a relative path so the config works on every machine. Hardcoded home directories break for every other contributor.
|
|
392
|
+
|
|
393
|
+
### `hygiene/settings-scope-conflict`
|
|
394
|
+
|
|
395
|
+
**Local settings silently override project settings** · `info`
|
|
396
|
+
|
|
397
|
+
Not a bug, but worth knowing: this key differs between the committed project config and your local override, so your session behaves differently from your teammates.
|
|
398
|
+
|
|
399
|
+
### `hygiene/keybindings-duplicate`
|
|
400
|
+
|
|
401
|
+
**Two actions bound to the same key** · `warning`
|
|
402
|
+
|
|
403
|
+
One of the two bindings will not fire. Pick a different chord for the loser.
|
|
404
|
+
|
|
405
|
+
## Policy
|
|
406
|
+
|
|
407
|
+
Enforcement of a written standard across more than one repository. These rules activate when an agentdoctor.policy.json is committed and are silent otherwise.
|
|
408
|
+
|
|
409
|
+
### `policy/missing-required-deny`
|
|
410
|
+
|
|
411
|
+
**Required deny rule is absent** · `error`
|
|
412
|
+
|
|
413
|
+
Add the rule to committed project settings. It is mandated by your agentdoctor.policy.json.
|
|
414
|
+
|
|
415
|
+
### `policy/forbidden-allow`
|
|
416
|
+
|
|
417
|
+
**Allow rule forbidden by policy** · `error`
|
|
418
|
+
|
|
419
|
+
Remove the rule or get the policy amended. Policy exists so this decision is made once, centrally, instead of per repo.
|
|
420
|
+
|
|
421
|
+
### `policy/unapproved-mcp-server`
|
|
422
|
+
|
|
423
|
+
**MCP server not on the approved list** · `error`
|
|
424
|
+
|
|
425
|
+
MCP servers run code and see your context. Add the server to allowedMcpServers in policy once it has been reviewed.
|
|
426
|
+
|
|
427
|
+
### `policy/required-hook-missing`
|
|
428
|
+
|
|
429
|
+
**Mandated guardrail hook is missing** · `error`
|
|
430
|
+
|
|
431
|
+
Policy requires this hook event to be configured. Copy it from your organisation template.
|
|
432
|
+
|
|
433
|
+
### `policy/memory-budget-exceeded`
|
|
434
|
+
|
|
435
|
+
**Always-on context exceeds the policy budget** · `error`
|
|
436
|
+
|
|
437
|
+
Trim the memory files or raise maxMemoryTokens deliberately. A context budget is the only thing that stops CLAUDE.md growing without limit.
|
|
438
|
+
|
|
439
|
+
### `policy/forbidden-permission-mode`
|
|
440
|
+
|
|
441
|
+
**Permission mode forbidden by policy** · `error`
|
|
442
|
+
|
|
443
|
+
Change defaultMode to a mode your policy permits.
|
|
444
|
+
|
|
445
|
+
### `policy/permission-drift`
|
|
446
|
+
|
|
447
|
+
**Local overrides widen the committed permission set** · `warning`
|
|
448
|
+
|
|
449
|
+
Local settings are invisible in review. If a rule is genuinely needed, put it in project settings so the team sees it; if it is personal, keep it narrow.
|
|
450
|
+
|
|
451
|
+
### `policy/file-invalid`
|
|
452
|
+
|
|
453
|
+
**Policy file could not be read** · `error`
|
|
454
|
+
|
|
455
|
+
A policy that fails to parse enforces nothing, which is the most dangerous state for a guardrail to be in.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
Suppress any rule for one file with a comment in that file:
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
agentdoctor-disable <rule-id>
|
|
463
|
+
```
|