Skip to content

fix(server): Claude task output tail ignores ids that are not a single path segment - #36216

Open
juliusmarminge wants to merge 9 commits into
mainfrom
factory/pr-36058-a1794586396
Open

juliusmarminge wants to merge 9 commits into
mainfrom
factory/pr-36058-a1794586396

Conversation

@juliusmarminge

Copy link
Copy Markdown
Member

tailClaudeTaskOutput joins the provider-supplied sessionId and taskId from the Claude SDK stream into ///tasks/.output. An id containing ../ could point the tail at a file outside Claude's per-user task directory and stream its contents into the thread. The fix returns early unless both ids match /^[\w-]+$/ (a single plain path segment). One regression test covers a traversing session id and a traversing task id; it fails on the unguarded code (both cases read the planted 'secret' file) and passes with the guard. The author's approach and commits are kept as-is; origin/main was merged in with no conflicts. Pushing to factory/pr-26501-a1794553696 was refused by the remote, so the work went to factory/pr-36058-a1794586396.

Fixes #26489

Supersedes #36058. The change is @juliusmarminge's; their commits are kept.

Reproduction on main

Derived from the issue, diff, and main's adapter code; not executed. In an isolated POSIX T3 server connected to a real client, configure a controlled Claude SDK stream fixture. Under the configured Claude task-output root, create a project directory, and beside that root create outside/tasks/bk6xreeye.output containing a harmless marker. Start a Claude thread whose fixture emits a Bash tool call and a matching system/task_started frame with task_type local_bash, tool_use_id matching the call, is_backgrounded false, session_id ../../outside, and task_id bk6xreeye. Inspect the command's live output: main streams the marker, whereas the fix omits it. Repeat with a valid UUID session_id and task_id ../../../../outside/tasks/bk6xreeye. Finally emit the matching normal tool result and confirm it still appears; use valid IDs and a file inside the normal task directory as a control.

Evidence

Being captured: before and after in the real app. This section is filled in before the pull request can merge.

Tests

vp test run src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts (apps/server): 6/6 pass; same with the guard line removed: new test fails (leaks 'secret' for both ids), restored afterwards; vp run check:changed: format, lint, knip, graphql, typecheck (3 packages), tests (2 files, 8 tests) all pass.

Not in this change

Who did the work

  • Written by Claude Opus 5.5, in Claude Code
  • Reviewed by GPT-6.1 Sol, in Codex, and by Claude Opus 5.5, each on its own
  • Proof captured by GPT-6.1 Sol and checked by Claude Opus 5.5

Run by the t3code factory in T3 Code.

juliusmarminge and others added 9 commits October 28, 2026 13:58
…ersing session ids

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e path segment

The tail joined the SDK's task_started session_id and task_id into a
filesystem path unchecked, so an id containing `..` could stream a file
outside the Claude task directory as command output. Skip tailing unless
both ids match `^[\w-]+$`.

Fixes #26489

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AI-assisted; verified with the tests and tools listed above.
AI-assisted; verified with the tests and tools listed above.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted Author is trusted by repo permissions or the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Nov 13, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 4511883

Macroscope's review found this PR approvable — The change rejects unsafe session and task IDs before reading Claude task-output files. Its impact is limited to live-output tailing for rejected IDs; final task results remain available.

You can add or adjust custom eligibility rules. Learn more.

@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.5 KiB 21.5 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 1 1 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Other scenarios

All 64 scenario measurements match main.

All 64 scenario measurements
Provider Scenario Metric Main baseline This PR Impact PR ceiling
Codex short turn resume WebSocket decoded 10.5 KiB 10.5 KiB 0 B (0.0%) 12.7 KiB ✅
Codex short turn resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex long turn running, cold open HTTP decoded 281.7 KiB 281.7 KiB 0 B (0.0%) 302.7 KiB ✅
Codex long turn running, cold open rows 75 75 0 (0.0%) 75 ✅
Codex long turn running, history page HTTP decoded 92.4 KiB 92.4 KiB 0 B (0.0%) 107.4 KiB ✅
Codex long turn running, history page rows 75 75 0 (0.0%) 75 ✅
Codex long turn finished, cold open HTTP decoded 381.5 KiB 381.5 KiB 0 B (0.0%) 410.2 KiB ✅
Codex long turn finished, cold open rows 151 151 0 (0.0%) 151 ✅
Codex long turn finished, socket cold open WebSocket decoded 381.6 KiB 381.6 KiB 0 B (0.0%) 410.2 KiB ✅
Codex long turn finished, socket cold open WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex long turn finished, socket cold open rows 151 151 0 (0.0%) 151 ✅
Codex long turn finished, work page HTTP decoded 186.2 KiB 186.2 KiB 0 B (0.0%) 200.2 KiB ✅
Codex long turn finished, work page rows 150 150 0 (0.0%) 150 ✅
Codex idle thread resume WebSocket decoded 65 B 65 B 0 B (0.0%) 200 B ✅
Codex idle thread resume WebSocket messages 1 1 0 (0.0%) 1 ✅
Codex fleet shell snapshot HTTP decoded 42.4 KiB 42.4 KiB 0 B (0.0%) 53.7 KiB ✅
Codex fleet shell snapshot rows 27 27 0 (0.0%) 27 ✅
Codex fleet shell subscribe WebSocket decoded 65 B 65 B 0 B (0.0%) 200 B ✅
Codex fleet shell subscribe WebSocket messages 1 1 0 (0.0%) 1 ✅
Codex fleet shell resume check HTTP decoded 18 B 18 B 0 B (0.0%) 100 B ✅
Codex fleet shell resume check rows 0 0 0 0 ✅
Codex stale shell reload HTTP decoded 42.4 KiB 42.4 KiB 0 B (0.0%) 53.7 KiB ✅
Codex stale shell reload rows 27 27 0 (0.0%) 27 ✅
Codex stale shell resume WebSocket decoded 140 B 140 B 0 B (0.0%) 400 B ✅
Codex stale shell resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex stale todo list cold open HTTP decoded 72.4 KiB 72.4 KiB 0 B (0.0%) 83.0 KiB ✅
Codex stale todo list cold open rows 75 75 0 (0.0%) 75 ✅
Codex stale todo list cold open nodes 2 2 0 (0.0%) 4 ✅
Codex stale todo list cold open subagents 2 2 0 (0.0%) 4 ✅
Codex stale todo list cold open plans 1 1 0 (0.0%) 2 ✅
Codex stale todo list cold open providerTurns 1 1 0 (0.0%) 2 ✅
Codex stale todo list cold open checkpoints 1 1 0 (0.0%) 2 ✅
Claude short turn resume WebSocket decoded 10.6 KiB 10.6 KiB 0 B (0.0%) 12.7 KiB ✅
Claude short turn resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude long turn running, cold open HTTP decoded 283.3 KiB 283.3 KiB 0 B (0.0%) 302.7 KiB ✅
Claude long turn running, cold open rows 75 75 0 (0.0%) 75 ✅
Claude long turn running, history page HTTP decoded 93.4 KiB 93.4 KiB 0 B (0.0%) 107.4 KiB ✅
Claude long turn running, history page rows 75 75 0 (0.0%) 75 ✅
Claude long turn finished, cold open HTTP decoded 384.2 KiB 384.2 KiB 0 B (0.0%) 410.2 KiB ✅
Claude long turn finished, cold open rows 151 151 0 (0.0%) 151 ✅
Claude long turn finished, socket cold open WebSocket decoded 384.3 KiB 384.3 KiB 0 B (0.0%) 410.2 KiB ✅
Claude long turn finished, socket cold open WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude long turn finished, socket cold open rows 151 151 0 (0.0%) 151 ✅
Claude long turn finished, work page HTTP decoded 188.1 KiB 188.1 KiB 0 B (0.0%) 200.2 KiB ✅
Claude long turn finished, work page rows 150 150 0 (0.0%) 150 ✅
Claude idle thread resume WebSocket decoded 66 B 66 B 0 B (0.0%) 200 B ✅
Claude idle thread resume WebSocket messages 1 1 0 (0.0%) 1 ✅
Claude fleet shell snapshot HTTP decoded 43.0 KiB 43.0 KiB 0 B (0.0%) 53.7 KiB ✅
Claude fleet shell snapshot rows 27 27 0 (0.0%) 27 ✅
Claude fleet shell subscribe WebSocket decoded 66 B 66 B 0 B (0.0%) 200 B ✅
Claude fleet shell subscribe WebSocket messages 1 1 0 (0.0%) 1 ✅
Claude fleet shell resume check HTTP decoded 18 B 18 B 0 B (0.0%) 100 B ✅
Claude fleet shell resume check rows 0 0 0 0 ✅
Claude stale shell reload HTTP decoded 43.0 KiB 43.0 KiB 0 B (0.0%) 53.7 KiB ✅
Claude stale shell reload rows 27 27 0 (0.0%) 27 ✅
Claude stale shell resume WebSocket decoded 140 B 140 B 0 B (0.0%) 400 B ✅
Claude stale shell resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude stale todo list cold open HTTP decoded 73.4 KiB 73.4 KiB 0 B (0.0%) 83.0 KiB ✅
Claude stale todo list cold open rows 75 75 0 (0.0%) 75 ✅
Claude stale todo list cold open nodes 2 2 0 (0.0%) 4 ✅
Claude stale todo list cold open subagents 2 2 0 (0.0%) 4 ✅
Claude stale todo list cold open plans 1 1 0 (0.0%) 2 ✅
Claude stale todo list cold open providerTurns 1 1 0 (0.0%) 2 ✅
Claude stale todo list cold open checkpoints 1 1 0 (0.0%) 2 ✅

Baseline: f313d0a · PR result: 4511883 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@coderabbitai

coderabbitai Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

Claude task output tailing now checks session and task IDs against a safe path-segment pattern before filesystem access. Tests cover traversal IDs and assert that they produce no output chunks.

Changes: Claude task output path validation

Layer / File(s) Summary
Validate IDs before tailing
apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts, apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts
The output tail checks session and task IDs against a safe path-segment pattern before filesystem access. Tests assert that traversal IDs produce no output chunks.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: saphid, abhishek-b-r

Merge Risk: Minimal · up to 45118

The change blocks path-traversal session and task IDs from reading files outside the Claude task output directory. No concrete merge-blocking defect was found. Adding a resolved-path containment check could be a reasonable follow-up.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the actual change: tailClaudeTaskOutput now rejects session or task IDs that are not a single path segment. It uses the conventional commit form with a scope. The title is specific and…
Description check Passed The description follows the template's main sections: Problem, Change, Scope, Verification, and an agent attribution line. The Problem and Change sections explain the path traversal issue and the guar…
Linked Issues check Passed [#26489] requires that provider-supplied session and task IDs be rejected when they are unsafe path components. At the reviewed head, tailClaudeTaskOutput returns before any filesystem access unless…
Out of Scope Changes check Passed The whole-PR diff (merge base f313d0a to head 4511883) touches two files only. ClaudeTaskOutputTail.ts adds the SAFE_PATH_SEGMENT constant and one early-return guard in tailClaudeTaskOutput. Cl…
Full details: Title check

Explanation

The title names the actual change: tailClaudeTaskOutput now rejects session or task IDs that are not a single path segment. It uses the conventional commit form with a scope. The title is specific and matches the raw summary and objectives. It is 83 characters, slightly long but acceptable.

Full details: Description check

Explanation

The description follows the template's main sections: Problem, Change, Scope, Verification, and an agent attribution line. The Problem and Change sections explain the path traversal issue and the guard clearly. Scope links issue #26489 and notes the supersession of #36058. Verification lists focused test runs and the regression test result. The Evidence section says proof capture is in progress and must be filled in before merge. The template requires before/after evidence for behavior changes, and the author marks the real-app reproduction as not executed. The description is mostly complete, but the open Evidence section and the unexecuted reproduction are gaps. The description also includes process notes (remote push refusal, branch naming) that do not belong in the template.

Resolution

Before merge, complete the Evidence section with the real-app before and after results, or state clearly what could not be checked. Remove or shorten the branch and push-refusal notes, since they do not describe the change. Confirm that the Scope section explains why no issue is needed beyond the linked issue #26489.

Full details: Linked Issues check

Explanation

[#26489] requires that provider-supplied session and task IDs be rejected when they are unsafe path components. At the reviewed head, tailClaudeTaskOutput returns before any filesystem access unless sessionId and taskId both match SAFE_PATH_SEGMENT = /^[\w-]+$/ (ClaudeTaskOutputTail.ts). Because . and / are not in \w, neither ID can contain .. or a separator, so path.join(root, project, sessionId, "tasks", taskId + ".output") cannot leave the session directory. The project segment comes from readDirectory(root), not from the stream. The issue also asks for an explicit containment check, as in CheckpointRestoreSafety. The PR does not add one. The regex makes that check redundant for these inputs, so the expected outcome (no escape from the session root) is met. The regression test reads nothing when a session or task id leaves the Claude task directory covers a traversing session ID and a traversing task ID, and it reads a planted file outside the task directory in both cases. The PR description reports that this test fails without the guard. This run did not execute the test, so that claim is taken from the PR description.

Full details: Out of Scope Changes check

Explanation

The whole-PR diff (merge base f313d0a to head 4511883) touches two files only. ClaudeTaskOutputTail.ts adds the SAFE_PATH_SEGMENT constant and one early-return guard in tailClaudeTaskOutput. ClaudeTaskOutputTail.test.ts adds one regression test for the same traversal cases. Both changes implement the ID validation required by #26489. No unrelated code, refactoring, or documentation changes appear in the diff. Generic PR-description material about the branch, merge, and push target is process information, not code scope.

Finishing Touches

Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Usage-based review receipt

Note

This review was completed with usage-based billing: files reviewed beyond your plan's included limits are billed at $0.25/file. View usage-based billing.

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between f313d0a and 4511883.

Files selected for processing: 2

  • apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.test.ts
  • apps/server/src/orchestration-v2/Adapters/ClaudeTaskOutputTail.ts

Limit details: You’ve used all 10 included reviews currently available.

@juliusmarminge

Copy link
Copy Markdown
Member Author

The automated pipeline stopped on this pull request and will not merge it:

the proof could not be captured: the agent's turn ended as failed twice

It stays open for a person to finish or close.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS 0-9 changed lines (additions + deletions). vouch:trusted Author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Claude background-task output tail reads filesystem paths built from unvalidated provider stream ids

1 participant