Skip to content

Daemon connection fails in sandboxed environments due to incorrect EPERM handling in pidlock #11164

@martijnwalraven

Description

@martijnwalraven

Summary

When running turbo commands inside a sandboxed environment (like Claude Code's macOS sandbox), connecting to an externally-started daemon fails due to two independent issues. Both need to be addressed for full sandbox compatibility.

Context

Claude Code is Anthropic's AI coding assistant that runs bash commands in a macOS sandbox (using sandbox-exec / Seatbelt). The sandbox:

  • Sets TMPDIR=/tmp/claude for write isolation
  • Blocks process signaling (kill syscalls return EPERM)
  • Allows Unix socket connections (configurable)

We want to connect to a turborepo daemon started outside the sandbox to benefit from caching and file watching capabilities.

Two Independent Issues

Issue 1: TMPDIR-Based Daemon Paths

Turborepo uses std::env::temp_dir() to locate daemon files. When TMPDIR differs between environments, the paths don't match:

  • External daemon: /var/folders/.../T/turbod/<hash>/
  • Sandboxed turbo: /tmp/claude/turbod/<hash>/

Current workaround: Start the daemon with matching TMPDIR:

TMPDIR=/tmp/claude turbo daemon start

Suggested fix: Use a fixed, predictable path for daemon files (e.g., ~/.turbo/daemon/ or ~/.cache/turborepo/daemon/) instead of relying on TMPDIR. This would:

  • Avoid TMPDIR mismatch issues entirely
  • Make daemon location predictable across environments
  • Match patterns used by other tools (Docker, npm, etc.)

Issue 2: Incorrect kill(pid, 0) EPERM Handling (Bug)

Even with matching paths, daemon detection fails. The pidlock verification in crates/turborepo-pidlock/src/lib.rs uses:

fn process_exists(pid: i32) -> bool {
    unsafe {
        let result = libc::kill(pid, 0);
        result == 0
    }
}

Per POSIX, kill(pid, 0) returns:

  • 0 → process exists AND caller can signal it
  • -1 with ESRCH → process does NOT exist
  • -1 with EPERM → process EXISTS but caller lacks permission

The sandbox returns EPERM (blocks signaling), but turborepo interprets this as "process doesn't exist," leading to:

  1. Pidfile marked as "stale"
  2. Pidfile deleted
  3. Daemon reported as "not running"
  4. Attempt to start new daemon (fails due to other sandbox restrictions)

There is no workaround for this issue — it requires a code fix.

Suggested fix: Update process_exists() to correctly handle EPERM:

fn process_exists(pid: i32) -> bool {
    unsafe {
        let result = libc::kill(pid, 0);
        if result == 0 {
            return true;
        }
        // EPERM means process exists but we can't signal it
        // ESRCH means process doesn't exist
        *libc::__errno_location() != libc::ESRCH
    }
}

Alternatively, consider using socket connection as the primary liveness check instead of kill, since the socket is the actual communication channel anyway.

Reproduction

  1. Start daemon externally:

    TMPDIR=/tmp/claude turbo daemon start
    TMPDIR=/tmp/claude turbo daemon status  # Shows running
  2. From sandboxed environment (or simulate with a restricted user):

    turbo daemon status
    # WARNING: stale pid file at "/tmp/claude/turbod/.../turbod.pid"
    # daemon is not running
  3. Verify daemon is actually alive:

    # Socket connection works!
    node -e "require('net').connect('/tmp/claude/turbod/.../turbod.sock').on('connect', () => console.log('alive'))"

Environment

  • Turbo version: 2.6.1
  • OS: macOS 15.4 (Sequoia)
  • Sandbox: Claude Code's Seatbelt-based sandbox

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: daemonIssues related to Turborepo's daemonarea:watchIssues about watch mode

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions