scan — AgentTeamsModule¶
Proactive security scanner for generated agent files.
Scans .agent.md and related files for: absolute paths containing usernames (PII exposure), credential patterns (API keys, tokens, passwords), unresolved auto-placeholders ({UPPER_SNAKE_CASE}), and unresolved manual placeholders ({MANUAL:*}).
scan_content() is content-only (no filesystem coupling), so it doubles as a review-time check: security.template.md Rules S-1 and S-8 cite it directly as the preferred way to verify a piece of reviewed content, with the existing manual-pattern bullets retained as a fallback for runtimes that can't execute Python.
Layout¶
- Module:
agentteams.scan(importable) - CLI:
python -m agentteams.scan <path>(a file, a directory tree, or-for stdin) — for a runtime with shell/executeaccess but no way to nativelyimportand callscan_contentdirectly.
Source:
agentteams/scan.py
Classes¶
ScanFinding¶
Source:
agentteams/scan.py
A single security finding.
Attributes:
file(str) — Relative path of the file containing the finding.line(int) — Line number (1-based).category(str) — Finding category (e.g.,'PII','credential','unresolved-placeholder').severity(str) —'high','medium', or'low'.message(str) — Human-readable description of the finding.snippet(str) — The offending line content (truncated).
ScanReport¶
Source:
agentteams/scan.py
Results of a security scan.
Attributes:
scanned_files(int) — Total number of files scanned.findings(list[ScanFinding]) — All findings across all files.
Properties:
has_issues(bool) —Trueif any findings exist.high_count(int) — Count of high-severity findings.medium_count(int) — Count of medium-severity findings.low_count(int) — Count of low-severity findings.verdict(str) —HALT/CONDITIONAL_PASS/PASS, computed fromself.findingsviaverdict_for_findings().
Constants¶
HALT, CONDITIONAL_PASS, PASS¶
Source:
agentteams/scan.py
The three verdict strings verdict_for_findings() returns (each defined in the Security Workflow Glossary). Mirror the HALT / CONDITIONAL PASS / PASS verdicts in security.template.md's escalation table — specifically the Credential, PII-path, and Machine-specific-information rows, plus (since 2026-07-31) Rule S-5's literal instruction-override/identity-override patterns and C-1's tier-claim patterns, the scan-derivable subset of that table. _check_injection() runs on every scanned line against _INJECTION_PATTERNS, _IDENTITY_OVERRIDE_PATTERNS, and _TIER_CLAIM_PATTERNS, emitting category="injection", severity="high" findings — which means a matched injection attempt is mechanized and HALT-triggering, not a judgment call left to the agent. What remains genuinely out of scan-derivable scope: destructive-op confirmation, external-repo writes, scope violations, and S-5's third bullet (a heading that redefines agent identity), which needs judgment rather than pattern matching.
Functions¶
scan_directory(agents_dir, *, expected_agent_names=None)¶
Source:
agentteams/scan.py
Scan all .agent.md, .md, and .json files in agents_dir for
security issues.
Args:
agents_dir(Path) — Path to the.github/agents/directory.expected_agent_names(set[str] | None, keyword-only) — (T3a.2 v4) When provided,.agent.mdfiles whose basename is NOT in this set are not skipped: they are scanned normally, like every other file, and additionally flagged as an"orphan-agent"finding atmediumseverity (non-blocking underverdict_for_findings, but no longer silent). Orphans were skipped entirely until 2026-08-06, which made an injected agent file the one class of agent file the scanner never read. Default:None— no orphan-flagging occurs, but scanning coverage is unaffected either way; every.agent.mdfound is always scanned regardless of this parameter.
Walk semantics:
- The
.agentteams-backups/subtree is always skipped. Point-in-time snapshots faithfully preserve historical (already-surfaced) content and should not gate the live scan. _OPERATIONAL_JSON_NAMES(build-log.json,delivery-receipt.json,memory-index.json,eval-suite.json,doc-hashes.json,security-vulnerability-watch.json) suppresses the absolute-path PII detector, the entropy-based detectors, and unresolved-placeholder detection in those files. These are pipeline-controlled artefacts that legitimately carry paths, content hashes, and indexed copies of documentation. Pattern-based credential detection (sk_live_*,xoxb-*, etc.) still applies. The exemption additionally requires the file to reside directly in the agents dir'sreferences/subdirectory (_is_operational_json, carved to the integrity-pinnedagentteams/scan_operational.py). Memory-index shards (memory-index.part-NNNN.json) take the same exemption only when the sibling shard manifest lists them with their current sha256 (seememory_index_store) — a basename match alone is not enough. This is a deliberate anti-spoofing design point: keying the exemption on basename alone would let an agent with write access pre-blind the scanner to its own payload just by choosing a matching filename anywhere else in the tree.- Placeholder matches that fall entirely inside an inline-code
span (
`…`) on the same line are skipped — those are documentation prose mentioning placeholder names, not real unresolved placeholders. _SECRET_CONTEXT_REis word-bounded so prose like "tokenized" or "authorize" does not elevate the line into secret-context scanning.
Returns: ScanReport
scan_content(content, filename='<string>')¶
Source:
agentteams/scan.py
Scan a string of content for security issues.
Args:
content(str) — File content to scan.filename(str) — Filename label for findings. Default:'<string>'.
Returns: list[ScanFinding]
print_scan_report(report)¶
Source:
agentteams/scan.py
Print a human-readable scan report to stdout.
Args:
report(ScanReport) — Result fromscan_directory().
verdict_for_findings(findings)¶
Source:
agentteams/scan.py
Map scan findings to a HALT / CONDITIONAL_PASS / PASS verdict: any high-severity finding is HALT; any finding at all (with no high) is CONDITIONAL_PASS; no findings is PASS.
Args:
findings(Iterable[ScanFinding]) — Findings to evaluate — typicallyscan_content()'s return value or aScanReport.findingslist.
Returns: str — One of HALT, CONDITIONAL_PASS, PASS.
CLI¶
# Scan a file, print JSON findings + verdict, exit 1 iff verdict is HALT
python -m agentteams.scan path/to/file.md
# Scan piped content (e.g. a diff) via stdin
git diff | python -m agentteams.scan -
A directory argument walks the tree. Symlinks are not followed, and only the root .git
object store is pruned; its config and hooks/ are still scanned. Every regular file goes
through scan_content, with non-UTF-8 bytes decoded lossily, and the findings are combined
into one verdict. The JSON adds files_scanned and skipped. Each skipped entry also raises
a medium scanner-evasion finding, so a skip can never read as PASS. A path that is neither
a file nor a directory (missing, FIFO, device) is a usage error (exit 2). A single file is also
decoded lossily, so non-UTF-8 content is scanned instead of crashing.
See Also¶
session_scan— a sibling review-time utility (repo at-large issue scan) with its ownpython -mentrypoint.integrity— the hash manifest that listsscan.pyamong the enforcement modules it protects.audit— the post-generation audit that runs alongside this proactive scan.redteam— the standing constitutional red-team battery this scanner feeds as an enforcement input.