Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,38 @@ describe("tailClaudeTaskOutput", () => {
assert.deepEqual(chunks, ["one\n", "two\n"]);
}).pipe(Effect.provide(NodeServices.layer)),
);

it.effect("reads nothing when a session or task id leaves the Claude task directory", () =>
Effect.scoped(
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const base = yield* fileSystem.makeTempDirectoryScoped({ prefix: "claude-task-output-" });
const root = path.join(base, "claude-root");
yield* fileSystem.makeDirectory(path.join(root, "-private-tmp-project"), {
recursive: true,
});
const outside = path.join(base, "outside", "tasks");
yield* fileSystem.makeDirectory(outside, { recursive: true });
yield* fileSystem.writeFileString(path.join(outside, `${TASK_ID}.output`), "secret\n");
const chunks: Array<string> = [];
const stop = yield* Deferred.make<void>();
yield* Deferred.succeed(stop, undefined);
for (const ids of [
{ sessionId: "../../outside", taskId: TASK_ID },
{ sessionId: SESSION_ID, taskId: `../../../../outside/tasks/${TASK_ID}` },
]) {
yield* tailClaudeTaskOutput({
root,
...ids,
stop,
onChunk: (chunk) => Effect.sync(() => chunks.push(chunk)),
});
}
assert.deepEqual(chunks, []);
}),
).pipe(Effect.provide(NodeServices.layer)),
);
});

describe("claudeTaskOutputRoot", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const CLAUDE_TASK_OUTPUT_POLL_MS = 250;
const MAX_LOCATE_ATTEMPTS = 40;
/** Skip ahead of fast writers; only the tail is displayed. */
const MAX_READ_BYTES = 256 * 1024;
/** Ids come from the SDK stream and are joined into a path; one plain segment keeps them inside root. */
const SAFE_PATH_SEGMENT = /^[\w-]+$/;

/** Claude Code's per-user temp root, or none where it has no uid (Windows). */
export function claudeTaskOutputRoot(
Expand Down Expand Up @@ -65,6 +67,7 @@ export const tailClaudeTaskOutput = Effect.fn("ClaudeTaskOutputTail.tail")(funct
readonly stop: Deferred.Deferred<void>;
readonly onChunk: (chunk: string) => Effect.Effect<void>;
}) {
if (!SAFE_PATH_SEGMENT.test(input.sessionId) || !SAFE_PATH_SEGMENT.test(input.taskId)) return;
const fileSystem = yield* FileSystem.FileSystem;
const pause = Deferred.await(input.stop).pipe(
Effect.as(true),
Expand Down
Loading