Skip to content

Log directory grows unbounded; cleanup is gated on a tool that's rarely invoked #385

@cameroncooke

Description

@cameroncooke

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)

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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/

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions