Subagent task-output symlink mtime is stale — causes false 'stalled' diagnoses

Resolved 💬 1 comment Opened Apr 18, 2026 by zackees Closed May 27, 2026

Summary

When a background Agent is spawned, Claude Code places a file at:

<TMP>/claude/<project-hash>/tasks/<agent-id>.output

This path is a symlink pointing at the real transcript in:

~/.claude/projects/<project-hash>/<session-id>/subagents/agent-<agent-id>.jsonl

The symlink itself has an mtime frozen at symlink creation time. The target file is updated live as the agent works. Agents and users who check liveness via ls -la / stat on the symlink (without -L) see a frozen mtime and conclude the agent is stalled, when in fact it is working normally.

Impact

Agents running long, legitimate work (5–45 min) may be incorrectly killed by a supervising agent or user who polls the symlink's mtime. I killed a live, productive subagent based on this misdiagnosis; 45 minutes of work was discarded because the 'stalled' signal looked unambiguous.

Reproduction

  1. In a Claude Code session, launch a long-running background agent (e.g., Agent({ subagent_type: 'general-purpose', run_in_background: true, prompt: '...' })). Note the returned agentId.
  2. The response text names the output file: output_file: <TMP>/claude/<hash>/tasks/<agent-id>.output.
  3. Wait 10+ minutes while the agent works.
  4. Compare:

``
$ stat -c '%y' <TMP>/claude/<hash>/tasks/<agent-id>.output
2026-04-17 20:39:30
$ stat -L -c '%y' <TMP>/claude/<hash>/tasks/<agent-id>.output
2026-04-17 21:24:32
$ ls -la <TMP>/claude/<hash>/tasks/<agent-id>.output
lrwxrwxrwx ... -> /home/.../subagents/agent-<id>.jsonl
``

The unfollowed mtime says 45 min stale; the followed mtime is current.

  1. In my session I did jq -r '.timestamp' agent-<id>.jsonl and confirmed 399 messages distributed across the 45 min — max gap between tool-call messages was 302 s. The agent was actively making progress the entire time.

Why this is subtle

  • Guidance in the tool result tells users: "Do NOT Read or Bash tail this file — it is the full sub-agent JSONL transcript". Users follow this — and thus rely on external signals like mtime to assess liveness.
  • The output path is presented as if it were a normal file. The fact that it is a symlink is not surfaced.
  • ls -la, stat, and common scripting idioms all report the symlink's own mtime unless -L is specified. This is standard POSIX behavior, not a bug in those tools.
  • Users don't see the ~/.claude/projects/.../subagents/ directory in any tool result; they have to discover it by reading the symlink target.

Suggested fixes (any one is sufficient)

  1. Write the transcript file directly at the <TMP>/.../tasks/<agent-id>.output path, or have the harness utime() the symlink on each transcript update so its mtime tracks the target. (The symlink approach only makes sense if it's maintained.)
  2. Expose an AgentStatus tool that returns {status, last_activity_ts, message_count} for a given agent_id so supervising agents have a reliable liveness signal without file-system probing.
  3. Rewrite the tool-result hint to explicitly warn: "To check liveness, use \stat -L -c %y <path>\ or AgentStatus(agent_id) — the symlink's own mtime is not a liveness indicator."

Environment

  • Claude Code CLI running on Windows 10 (MSYS2 bash / git-bash)
  • Claude Code version: claude-opus-4-7[1m] model, current release
  • Two subagents spawned concurrently; confirmed behavior on both.

Evidence

Full transcripts are at agent-af381eba8b6dd2535.jsonl (45 min, killed prematurely) and agent-a98281f14b92904c9.jsonl (40 min, completed normally, delivered PR FastLED/fbuild#90). Both show steady tool-call activity in the .jsonl with stat -L mtimes current to within seconds, while stat (no -L) reports mtimes from spawn time.

Thanks — happy to share the transcripts if useful.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗