[Bug]: Session scan reading a TCC-protected .git/config exhausts the libuv pool and hangs the server#15971

opened24d agobyAazgul

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Run the server headless on macOS (t3 service), with a Claude or Codex transcript whose cwd is a git repo under ~/Documents (or ~/Desktop) that the t3 binary has no privacy (TCC) consent for.
  2. Connect a client that runs the agent-session scan (project import).

Deterministic repro without TCC: make <repo>/.git/config a FIFO (mkfifo) and point a transcript at <repo>.

Expected behavior

The scan skips git identity for a directory it can't read, and the server stays responsive.

Actual behavior

AgentSessionScanner.readGitIdentity reads .git/config with readFile. open() waits on a privacy prompt nobody can answer, so it never returns. Fiber interruption doesn't release the libuv pool thread, and the client retries the scan. After four attempts the pool is full and every fs and DNS operation queues forever: HTTP routes hang (except ones that answer without I/O, like /mcp returning 401), the client shows "reconnecting", and telemetry fails with ConnectTimeoutError.

Evidence from the live process (0.0.45):

  • sample: all four libuv-worker threads in node::fs::ReadFileJob::DoThreadPoolWork → uv_fs_open → __open; the main thread is idle in kevent.
  • Inspector (Runtime.queryObjects on ReadFileJob): 4 jobs reading ~/Documents/<repo>/.git/config, 158 queued reads of secrets/cloud-publish-agent-activity.bin (from getDescriptor), and 199 queued GetAddrInfoReqWrap.

Impact

Blocks work completely

Version or commit

0.0.45 (also present on main @ 4ee6bfd5)

Environment

macOS 26.6.2 (arm64), t3 service (launchd), Node v26.8.2 runtime

Workaround

Move transcripts whose cwd is under a protected folder out of ~/.codex/sessions / ~/.claude/projects, or grant the t3 binary access, then run t3 service restart.

Note

Grok responding on behalf of Julius.

Triage

Thanks @azgul for the thorough write-up. The FIFO repro and the libuv analysis made this much easier to trace. Current main (cf3e714b0, which includes 4ee6bfd5) matches what you describe, and I didn't find an existing issue for it.

What I found

  • AgentSessionScanner.readGitIdentity (apps/server/src/project/AgentSessionScanner.ts) reads .git/config with fileSystem.readFileString on the shared libuv pool. There's no timeout and nothing isolating it from the rest of the pool.
  • readFileString is fs.readFile. The AbortSignal that Effect passes only skips the callback and doesn't free a worker already stuck inside uv_fs_open. A macOS TCC consent wait, or a FIFO, never returns, so that pool thread is lost for the life of the process.
  • The usage scanner already treats the 4-thread pool as a hard limit. Its FIFO tests keep a thread free on purpose (apps/server/src/usage/UsageService.test.ts, usageTranscriptReader.ts).
  • Each stuck worker comes from a separate agentSessions.scan call, such as a reload, a reconnect that restarts the import-step query in apps/web/src/state/agentSessions.ts, or another client. After the fourth, every later fs and dns.lookup waits forever. That matches the queued secrets/cloud-publish-agent-activity.bin reads, since ServerEnvironment.getDescriptor reads that secret on every descriptor request. Routes that answer before touching the filesystem or DNS (such as /mcp returning 401) stay up, while telemetry fails with ConnectTimeoutError.

Likely fix area

An Effect.timeout around the read alone wouldn't help, because the worker stays occupied after the fiber is interrupted. Options include doing the git identity read somewhere a hang can't pin the shared pool, such as a separate worker or child process, or skipping or limiting reads that can block. Either way, a scan that can't read git identity could fall back to NotGit and still finish. Until then, your workaround of moving the transcripts or granting TCC access and then running t3 service restart is the way to recover.

A maintainer will decide on the fix direction.