agentdoctor

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.

OptionTypeDefaultEffect
includeUserScopebooleantrueAlso scan ~/.claude
homestringos.homedir()Override the home directory (useful in tests)
policyPathstringauto-detectExplicit policy file path
onlystring[]allRestrict to categories or rule ids
disabledstring[]noneSkip rules or categories
minSeverity`'error'\'warning'\'info'`'info'Severity floor
baselineSet<string>emptyFingerprints 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 own rules array 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?) — reads agentdoctor.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.