Summary
~/Library/Developer/XcodeBuildMCP/logs/ grows unbounded for typical users because the only retention/cleanup path is gated on the simulator-log-capture flow (startLogCapture), which most non-log-capture invocations never hit. Build, test, run, and SPM flows all write to this directory but none of them prune it. As a result, log files older than the documented 3-day retention persist indefinitely until/unless the user happens to start a simulator log capture session.
Evidence (real machine state)
$ du -sh ~/Library/Developer/XcodeBuildMCP/logs/
720M /Users/cameroncooke/Library/Developer/XcodeBuildMCP/logs/
$ ls ~/Library/Developer/XcodeBuildMCP/logs/ | wc -l
55369
$ ls -lt ~/Library/Developer/XcodeBuildMCP/logs/ | tail -2
-rw-r--r-- 1 … 958 Apr 11 18:15 build_run_sim_parser-debug_2026-04-11T17-15-57-037Z.log
-rw-r--r-- 1 … 6826 Apr 11 18:15 build_run_sim_2026-04-11T17-15-51-978Z_pid90322.log
$ find ~/Library/Developer/XcodeBuildMCP/logs/ -type f -size 0 | wc -l
21118
- 55,369 files, 720 MB total
- Oldest file dates to 2026-04-11 — 20 days old, well past the 3-day retention
- 38 % of files are zero-byte (21,118 empties), suggesting log files are also being created speculatively for runs that never produce output
File-name-prefix breakdown (top entries):
4594 build_run_sim
4372 build_sim
3832 test_macos
3198 build_run_macos
3141 build_run_spm
3123 build_macos
3110 io.sentry.calculatorapp
3109 io.sentry.calculatorapp_oslog
3055 test_sim
2485 swift_package_test
2419 build_device
2342 build_spm
2187 test_device
2175 build_run_device
2109 test_sim_parser-debug
2051 build_run_sim_parser-debug
The two io.sentry.calculatorapp* buckets (~6,200 files combined) are from start_sim_log_cap-style flows. The remaining ~50,000 files are from build/run/test commands that never trigger the retention sweep.
Root cause
src/utils/log_capture.ts:14-19:
/**
* Log file retention policy:
* - Old log files (older than LOG_RETENTION_DAYS) are automatically deleted from the temp directory
* - Cleanup runs on every new log capture start
*/
const LOG_RETENTION_DAYS = 3;
src/utils/log_capture.ts:374-405:
async function cleanOldLogs(fileSystem: FileSystemExecutor): Promise<void> {
const logsDir = APP_LOG_DIR;
…
await Promise.all(
fileNames
.filter((f) => f.endsWith('.log'))
.map(async (f) => {
const filePath = path.join(logsDir, f);
try {
const stat = await fileSystem.stat(filePath);
if (now - stat.mtimeMs > retentionMs) {
await fileSystem.rm(filePath, { force: true });
…
Call-site search:
$ grep -rn "cleanOldLogs" src --include="*.ts" | grep -v __tests__
src/utils/log_capture.ts:92: await cleanOldLogs(fileSystem);
src/utils/log_capture.ts:374:async function cleanOldLogs(fileSystem: FileSystemExecutor): Promise<void> {
The single call site is inside startLogCapture (src/utils/log_capture.ts:79-92). Build, test, run, SPM flows write to APP_LOG_DIR (via log-paths.ts) but never call cleanOldLogs. So a user who builds/tests but rarely starts a simulator log capture session — i.e., most CLI users — will never see any pruning at all.
A secondary concern: when cleanOldLogs does run, it does Promise.all(fileNames.filter(...).map(stat+rm)) over every .log file in the directory. With 55,000+ files that's 55,000 stat syscalls per log-capture start, before any user work begins. As the directory grows, this becomes self-aggravating.
Suggested fixes (owner's call)
- Hoist
cleanOldLogs to a shared lifecycle hook that runs on every CLI invocation that opens a log file, not just startLogCapture. The cheapest version is to call it once on daemon/MCP-server startup and on each xcodebuildmcp … CLI run that writes to APP_LOG_DIR. Idempotent and bounded.
- Cap the sweep work: skip cleanup if it ran in the last N minutes (write a
.last-cleanup marker file), or sample a bounded number of files per run instead of scanning the entire directory.
- Stop creating zero-byte log files speculatively: only open the file (or use lazy-open via a write-guarded stream) once the first byte of output is actually available. 21k empty files is essentially noise.
- Consider a hard file count cap in addition to age-based retention (e.g., "keep newest 1000, delete the rest"). Age-based alone fails when retention isn't actually run.
- Optional UX: add a
xcodebuildmcp logs prune (or --clean-logs) explicit command so users can recover disk without running a log-capture session.
Environment
- macOS 26.3.1 (25D2128)
- xcodebuildmcp via
/opt/homebrew/bin/xcodebuildmcp (global npm install, latest)
- Logs directory:
~/Library/Developer/XcodeBuildMCP/logs/
Summary
~/Library/Developer/XcodeBuildMCP/logs/grows unbounded for typical users because the only retention/cleanup path is gated on the simulator-log-capture flow (startLogCapture), which most non-log-capture invocations never hit. Build, test, run, and SPM flows all write to this directory but none of them prune it. As a result, log files older than the documented 3-day retention persist indefinitely until/unless the user happens to start a simulator log capture session.Evidence (real machine state)
File-name-prefix breakdown (top entries):
The two
io.sentry.calculatorapp*buckets (~6,200 files combined) are fromstart_sim_log_cap-style flows. The remaining ~50,000 files are from build/run/test commands that never trigger the retention sweep.Root cause
src/utils/log_capture.ts:14-19:src/utils/log_capture.ts:374-405:Call-site search:
The single call site is inside
startLogCapture(src/utils/log_capture.ts:79-92). Build, test, run, SPM flows write toAPP_LOG_DIR(vialog-paths.ts) but never callcleanOldLogs. So a user who builds/tests but rarely starts a simulator log capture session — i.e., most CLI users — will never see any pruning at all.A secondary concern: when
cleanOldLogsdoes run, it doesPromise.all(fileNames.filter(...).map(stat+rm))over every.logfile in the directory. With 55,000+ files that's 55,000 stat syscalls per log-capture start, before any user work begins. As the directory grows, this becomes self-aggravating.Suggested fixes (owner's call)
cleanOldLogsto a shared lifecycle hook that runs on every CLI invocation that opens a log file, not juststartLogCapture. The cheapest version is to call it once on daemon/MCP-server startup and on eachxcodebuildmcp …CLI run that writes toAPP_LOG_DIR. Idempotent and bounded..last-cleanupmarker file), or sample a bounded number of files per run instead of scanning the entire directory.xcodebuildmcp logs prune(or--clean-logs) explicit command so users can recover disk without running a log-capture session.Environment
/opt/homebrew/bin/xcodebuildmcp(global npm install, latest)~/Library/Developer/XcodeBuildMCP/logs/