Describe the bug
apm audit --ci walks the entire root of every dedicated target (.claude/, .codex/, .cursor/, ...) for the hidden-Unicode signal of content-integrity. At user scope those roots are not deploy trees: ~/.claude/ holds projects/**/*.jsonl session transcripts and history.jsonl, ~/.codex/ holds session logs. APM never deploys anything there, yet the audit regex-scans every byte of it on every run.
Two consequences on a normal developer machine (783 MB under ~/.claude, 4.9 GB under ~/.codex):
- Time. The audit takes ~2m30s of CPU (
sre_search over TextIOWrapper.read, sampled). Anything that runs apm audit --ci with a bounded deadline times out.
- False positives.
content-integrity failed with 22 "critical" findings, all in .claude/history.jsonl and .claude/projects/**/*.jsonl. None of those files is deployed, none is in the lockfile, and apm audit --strip is not a sane remedy for a session transcript. --ci exits non-zero for content the audit cannot own.
Cause: _scan_deployed_trees (src/apm_cli/security/file_scanner.py) derives its walk roots from install_governance(), which by design returns the whole root for dedicated targets ("Dedicated target roots govern their full subtree") and only partitions the shared .agents/ root by primitive subdirectory. That is the right scope for ownership tests, but SecurityGate.scan_files then os.walks the whole root with no pruning, no size cap, and no extension filter. Introduced by #2381 (fix for #2379), which widened the scan from deployed_files to the governed tree so that unrecorded deployed files are covered. The widening is correct; the boundary it picked is one level too coarse.
Still present on main (4aaa73cd): file_scanner.py and _check_content_integrity are unchanged since #2381; gate.py only gained __pycache__ exclusions. No open issue or PR covers this.
To Reproduce
- Have a global APM workspace (
~/.apm/apm.yml + lockfile) with the claude target and a few skills installed.
- Use Claude Code or Codex for a while so
~/.claude/projects/ and ~/.codex/ hold transcripts (or seed them: mkdir -p ~/.claude/projects/x && head -c 500M /dev/urandom | base64 > ~/.claude/projects/x/s.jsonl).
cd ~/.apm && time apm audit --ci --format json
Observed: minutes of wall time; content-integrity reports findings under .claude/projects/... and .claude/history.jsonl.
Expected behavior
The hidden-Unicode walk covers each target's primitive deploy directories and generated files (.claude/skills/, .claude/agents/, .claude/commands/, .claude/hooks/, .claude/rules/, .codex/hooks.json, .agents/skills/, ...), exactly the way .agents/ is already partitioned. That keeps the #2379 guarantee (an unrecorded file under a deploy directory is still scanned) while leaving session data alone. Lockfile-claimed paths outside those directories (for example .claude/settings.json) stay covered by the lockfile-scoped scan, which is unioned with the tree walk today.
With that change on the same machine: 5s CPU, content-integrity passes, drift results identical.
Environment
- OS: macOS 26 (Darwin 25.6.0), arm64
- Python Version: 3.12
- APM Version: 0.29.0 (
656f3d6), also verified against main 4aaa73cd
Logs
$ cd ~/.apm && time apm audit --ci --format json
...
138.26s user 6.41s system 92% cpu 2:37.16 total
$ ps -o etime,%cpu,command -p <pid>
01:52 98.8 .../apm audit --ci --format json
# sampled: sre_search <- _io_TextIOWrapper_read, no open FDs besides transcripts
Before/after on the same workspace, apm audit --ci --format json:
|
wall |
CPU |
content-integrity |
| main |
2m37s |
138s |
22 findings, all .claude/projects/**/*.jsonl + .claude/history.jsonl |
| bounded walk |
25s (5s idle machine) |
5s |
0 findings |
Additional context
I have the fix ready (small: a bounded= keyword on install_governance() that applies the existing .agents/ partitioning to every root, used only by _scan_deployed_trees; ownership/prune callers unchanged; tests + docs). Will open the PR against this issue. Happy to adjust the boundary if maintainers prefer a different one (for example an explicit skip list per target instead of primitive partitioning).
Describe the bug
apm audit --ciwalks the entire root of every dedicated target (.claude/,.codex/,.cursor/, ...) for the hidden-Unicode signal ofcontent-integrity. At user scope those roots are not deploy trees:~/.claude/holdsprojects/**/*.jsonlsession transcripts andhistory.jsonl,~/.codex/holds session logs. APM never deploys anything there, yet the audit regex-scans every byte of it on every run.Two consequences on a normal developer machine (783 MB under
~/.claude, 4.9 GB under~/.codex):sre_searchoverTextIOWrapper.read, sampled). Anything that runsapm audit --ciwith a bounded deadline times out.content-integrityfailed with 22 "critical" findings, all in.claude/history.jsonland.claude/projects/**/*.jsonl. None of those files is deployed, none is in the lockfile, andapm audit --stripis not a sane remedy for a session transcript.--ciexits non-zero for content the audit cannot own.Cause:
_scan_deployed_trees(src/apm_cli/security/file_scanner.py) derives its walk roots frominstall_governance(), which by design returns the whole root for dedicated targets ("Dedicated target roots govern their full subtree") and only partitions the shared.agents/root by primitive subdirectory. That is the right scope for ownership tests, butSecurityGate.scan_filesthenos.walks the whole root with no pruning, no size cap, and no extension filter. Introduced by #2381 (fix for #2379), which widened the scan fromdeployed_filesto the governed tree so that unrecorded deployed files are covered. The widening is correct; the boundary it picked is one level too coarse.Still present on
main(4aaa73cd):file_scanner.pyand_check_content_integrityare unchanged since #2381;gate.pyonly gained__pycache__exclusions. No open issue or PR covers this.To Reproduce
~/.apm/apm.yml+ lockfile) with theclaudetarget and a few skills installed.~/.claude/projects/and~/.codex/hold transcripts (or seed them:mkdir -p ~/.claude/projects/x && head -c 500M /dev/urandom | base64 > ~/.claude/projects/x/s.jsonl).cd ~/.apm && time apm audit --ci --format jsonObserved: minutes of wall time;
content-integrityreports findings under.claude/projects/...and.claude/history.jsonl.Expected behavior
The hidden-Unicode walk covers each target's primitive deploy directories and generated files (
.claude/skills/,.claude/agents/,.claude/commands/,.claude/hooks/,.claude/rules/,.codex/hooks.json,.agents/skills/, ...), exactly the way.agents/is already partitioned. That keeps the #2379 guarantee (an unrecorded file under a deploy directory is still scanned) while leaving session data alone. Lockfile-claimed paths outside those directories (for example.claude/settings.json) stay covered by the lockfile-scoped scan, which is unioned with the tree walk today.With that change on the same machine: 5s CPU,
content-integritypasses, drift results identical.Environment
656f3d6), also verified againstmain4aaa73cdLogs
Before/after on the same workspace,
apm audit --ci --format json:.claude/projects/**/*.jsonl+.claude/history.jsonlAdditional context
I have the fix ready (small: a
bounded=keyword oninstall_governance()that applies the existing.agents/partitioning to every root, used only by_scan_deployed_trees; ownership/prune callers unchanged; tests + docs). Will open the PR against this issue. Happy to adjust the boundary if maintainers prefer a different one (for example an explicit skip list per target instead of primitive partitioning).