Programmatic API
agentdoctor is an ES module with no dependencies, so embedding it is one import.
import { run } from '@jqntn/agentdoctor';
const result = run('/path/to/repo', { includeUserScope: false });
for (const finding of result.findings) {
console.log(finding.severity, finding.ruleId, `${finding.display}:${finding.line}`);
}
process.exitCode = result.findings.some((f) => f.severity === 'error') ? 1 : 0;
run(root, options?)
The one-call entry point: discovers config, loads any team policy, runs every rule.
| Option | Type | Default | Effect | ||
|---|---|---|---|---|---|
includeUserScope | boolean | true | Also scan ~/.claude | ||
home | string | os.homedir() | Override the home directory (useful in tests) | ||
policyPath | string | auto-detect | Explicit policy file path | ||
only | string[] | all | Restrict to categories or rule ids | ||
disabled | string[] | none | Skip rules or categories | ||
minSeverity | `'error'\ | 'warning'\ | 'info'` | 'info' | Severity floor |
baseline | Set<string> | empty | Fingerprints to suppress |
Returns { findings, ran, suppressed, workspace, elapsedMs, version }.
Finding shape
{
ruleId: string; // e.g. "security/unrestricted-bash"
severity: 'error' | 'warning' | 'info';
category: 'correctness' | 'security' | 'cost' | 'hygiene' | 'policy';
message: string; // what is wrong, specific to this occurrence
help?: string; // what to do instead, from the rule
file: string; // absolute path
display: string; // repo-relative (or ~/) path for humans
line: number; // 1-based
column?: number;
configPath?: string; // e.g. "permissions.allow[0]"
snippet?: string; // offending value, secrets redacted
}
Findings are pre-sorted by severity, then file, then line.
Lower-level building blocks
All exported from the package root:
discover(root, { includeUserScope, home })— collects and parses every config file, with per-value source positions. Never opens credential files.lint(workspace, { rules, disabled, minSeverity, baseline })— runs rules over a discovered workspace. Pass your ownrulesarray to run a custom subset or add your own rules.fingerprint(finding)— the stable identity used by baselines. Anchored to the offending value, then config path, then message hash — never line numbers.allRules,CATEGORIES— the catalogue.loadPolicy(root, explicitPath?)— readsagentdoctor.policy.json.helpers— utilities handed to rules:parsePermission,estimateTokens, position lookup.
Writing a custom rule
A rule is a plain object; lint accepts any array of them.
import { discover, lint, allRules, helpers } from '@jqntn/agentdoctor';
const noOpusInProjects = {
id: 'org/no-opus-model',
category: 'policy',
severity: 'warning',
title: 'Project pins an Opus-tier model',
help: 'Our org standard is sonnet for project config; sessions can override per run.',
check({ files, report, helpers }) {
for (const file of files) {
if (file.kind !== 'settings' || file.data?.model !== 'opus') continue;
const position = helpers.at(file, 'model');
report({ file, line: position.line, column: position.column,
configPath: 'model', message: 'model is pinned to opus.' });
}
},
};
const workspace = discover(process.cwd(), { includeUserScope: false });
const result = lint(workspace, { rules: [...allRules, noOpusInProjects] });
The check function receives { workspace, files, report, helpers }. Throwing inside a rule does not crash the run — it surfaces as an internal/rule-crashed warning finding.
Stability
The exported API surface is small on purpose: run, discover, lint, fingerprint, allRules, CATEGORIES, loadPolicy, helpers, VERSION. Anything not exported from the package root is internal and may change without notice.