microsoft / microsoft/apm

[BUG] audit --ci walks the whole target root (session transcripts under ~/.claude, ~/.codex): minutes of CPU and false content-integrity findings

Open
#2,961 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area/audit-policy area/cli status/accepted triage/recommended type/bug
Dominant language
Python
Stars
3.8k
Forks
362
Avg merge
1d 17h
Merged PRs (30d)
132

Description

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):

  1. 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.
  2. 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

  1. Have a global APM workspace (~/.apm/apm.yml + lockfile) with the claude target and a few skills installed.
  2. 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).
  3. 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).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/apm_cli/security/file_scanner.py, especially _scan_deployed_trees and SecurityGate.scan_files, then trace install_governance() and _check_content_integrity. Review the existing ownership and scan tests before changing only the deployed-tree scan boundary. Done means primitive deploy directories and generated files remain covered, session transcripts are skipped, lockfile-scoped paths remain covered, and the reported audit findings and runtime are corrected.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.