Conversation
… source line A long launch line typed while the fresh pane shell is still busy waits in the terminal's canonical line buffer, which drops input past about 1,024 bytes on macOS, so the pane was left at an unfinished command with no agent running. fm-spawn now writes the assembled command to the task's own temp root under umask 077 and types only a short line that sources it. Refs kunchenguid#4559
…ch command The root lives at a predictable path under /tmp and now holds the whole launch command. Create it with mode 0700, refuse one that already exists as anything but a directory owned by this user that nobody else can write, and tighten an owned one, so no other local user can plant or swap the staged file. Refs kunchenguid#4559
|
Independent real-incident reproduction on Darwin, confirming this PR's fix direction. A Claude-harness worker launched through the Herdr backend never started. Firstmate typed a 1,115-byte launch command into the projected pane as one canonical-mode PTY line, and Darwin's line discipline retained exactly the first 1,024 bytes and silently discarded the remaining 91 — the closing command-substitution and the launch-brief path. The preserved pane capture ends exactly at the 1,024-byte boundary (mid-token at Two behaviors masked it as a started worker: the pane write was accepted (the kernel accepted the write even though canonical mode dropped the suffix), and the spawn record was published before any agent process was confirmed. That matches this PR's two parts — stage the full command in a private file and type only a short invocation, and verify the worker process actually started. The staging half removes the truncation; the verification half is what turns a future silent truncation (or any failed launch) into a loud failure instead of a phantom "started" worker. Cross-reference: herdrdev/herdr#2862 independently documents the same 1,024-byte canonical boundary. |
kunchenguid
left a comment
There was a problem hiding this comment.
I found one security invariant that still needs to be enforced before approval: the staged file itself must be forced to mode 0600, not only created under umask 077. printf ... >"$LAUNCH_FILE" preserves the mode of an existing launch.sh, so a reused private task root containing a pre-existing 0644 file leaves the full launch command readable by other users. Please reject or replace an existing non-regular/symlink launch.sh and explicitly chmod 0600 after creating/replacing it (with an appropriate failure path), then add a behavioral regression covering a pre-existing loose-mode launch file. The 0700 root check is good, and teardown already removes the recorded task root.
kunchenguid
left a comment
There was a problem hiding this comment.
The requested hardening is now present on #4665. Staging writes a fresh temporary file, forces mode 0600, atomically replaces a regular existing launch.sh, and refuses symlinks or non-regular paths. The Kimi behavior test covers a pre-existing mode-0644 launch.sh and verifies the final mode is 0600. The existing 0700 task-root checks, teardown cleanup, shared spawn_send_literal path (including Herdr), and live truncation evidence remain satisfactory. Local validation passed: FM_GATE_REFUSE_BYPASS=1 bash tests/fm-kimi-harness.test.sh and bin/fm-lint.sh.
|
Landed the same approach (with a bit more hardening on our side) in #4994 — thanks for the staging design and the live evidence. Closing this fork PR as superseded by that merge; happy to take follow-ups if anything still looks off on main. |
|
Superseded by #4994 (merged). |
Intent
Upstream issue #4559 reports that starting a worker sometimes leaves its terminal holding a mangled, unfinished command with no agent running.
Check that issue, and if it needs a fix, contribute one as a pull request to the upstream project.
Context needed to read that ask:
The issue is open and unfixed on the current default branch.
A maintainer labelled it
ready-for-pr, classed itrestore, and credited the finding that the launch command is being truncated by the terminal's canonical line buffer at about 1,024 bytes.That maintainer comment names this exact approach as in scope: "A short typed source of a private per-task
launch.sh(mode 0600) ... is in scope for a contributor PR. Prefer proof over fixed sleeps."The same comment sets a security condition on it: the staged file must keep mode 0600, must live in a task-private path, must not outlive the task, and must not leak into a shared temporary location.
A competing pull request #4598 is already open with the same diagnosis but a different mechanism, waiting for the pane's terminal to leave canonical mode with a configurable timeout.
This contribution stays on its own approach and does not duplicate that one's work.
The repair itself has already been written and proven locally; the deliverable here is getting it reviewed and opened as a pull request, not redesigning it.
What Changed
bin/fm-spawn.shnow writes the full launch command to/tmp/fm-<id>/launch.shwith mode 0600, inside the task's existing temp root. Instead of typing the whole command into the new pane, it types one short line,. '<launch.sh>', which runs the file. The terminal's line buffer on macOS drops input past about 1,024 bytes, so a long typed command could arrive unfinished and no agent would start./tmp/fm-<id>is now created with mode 0700. If the directory already exists, the spawn reuses it only when it's a real directory owned by the current user that nobody else can write to, and then tightens it to 0700. Otherwise it stops with an error before writing the launch file or launching anything.launch.shis 0600 and the pane gets the short line. A new test covers the refusal of a world-writable temp root and the tightening of an owned one.🤖 Generated with Claude Code
Risk Assessment
✅ Low: A small, contained change: fm-spawn now types a short
. '/tmp/fm-<id>/launch.sh'line instead of the whole launch command. The file is written under umask 077 (mode 0600) inside a task root that is checked to be the user's own and set to 0700, and fm-teardown already deletes that root. It has only one call site, and the security conditions from the issue hold.Testing
I built a live driver that runs the real bin/fm-spawn.sh (secondmate, codex harness) with a real tmux server on a private socket. It uses a fake codex that records its arguments, and a home path long enough to push the launch command past 1,024 bytes. Because this runs inside the no-mistakes gate, the driver unsets NO_MISTAKES_GATE and sets the project's own test switch FM_GATE_REFUSE_BYPASS=1; without them, fm-spawn refuses to run here. On the base commit, a pane shell that is still busy when the text arrives ended up holding a command cut off at 1,024 bytes, with no agent running. That is the reported bug. On the target commit, the same run started the agent with its full 7,989 bytes of arguments. A second base/target pair, where the pane shell only waited before starting, did not reproduce the bug on base; both commits started the agent in that pair. In that pair and in every target run, the staged file was 0600 and its folder 0700. The adversarial runs also passed: a world-writable or symlinked task temp folder was refused before anything was written or launched, a 0755 folder owned by this user was tightened to 0700, and teardown removed the task temp folder and launch.sh. The changed kimi harness suite also passed. I removed all temp worlds, the base extract and the tmux servers; the worktree is clean. There is no UI surface, so the evidence is CLI transcripts rather than screenshots.. '/tmp/fm-<id>/launch.sh'line was typed, FAKE-CODEX-STARTED, argv 7989 bytes; base-busyread-run.txt on 9f8ad95 under the same conditions: 1,024-byte cut-off comman…staged file: -rw------- vytautas ...; task root: drwx------Evidence: Live driver script (real tmux + real fm-spawn.sh)
Source: Live driver script (real tmux + real fm-spawn.sh)
Evidence: Base 9f8ad95 reproduces #4559: command cut off at 1,024 bytes, agent did not start
Source: Base 9f8ad95 reproduces #4559: command cut off at 1,024 bytes, agent did not start
staged file: none (launch typed directly) ... FM_HOME='/private/tmp/.../long-secondmate-home-path-segment-long-second AGENT DID NOT START pending command-line bytes in pane: 1035 pane foreground: bashEvidence: Target 85129fd under the same conditions: short source line, agent started
Source: Target 85129fd under the same conditions: short source line, agent started
staged file: -rw------- vytautas size=1384 bytes; task root: drwx------ bash-3.2$ . '/tmp/fm-lv89750/launch.sh' FAKE-CODEX-STARTED AGENT STARTED: fake codex ran with argc=2; argv bytes=7989Evidence: World-writable task temp root refused
Source: World-writable task temp root refused
Evidence: Symlinked task temp root refused, nothing written to the symlink's target
Source: Symlinked task temp root refused, nothing written to the symlink's target
Evidence: Own 0755 task temp root tightened to 0700 and launch succeeds
Source: Own 0755 task temp root tightened to 0700 and launch succeeds
Evidence: Teardown removes /tmp/fm-<id> and launch.sh
Source: Teardown removes /tmp/fm-<id> and launch.sh
Evidence: Sleep-only pane on target (both commits started the agent in this mode)
Source: Sleep-only pane on target (both commits started the agent in this mode)
Evidence: Sleep-only pane on base (agent started; the bug did not show in this mode)
Source: Sleep-only pane on base (agent started; the bug did not show in this mode)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
. '/tmp/fm-<id>/launch.sh'line was typed, FAKE-CODEX-STARTED, argv 7989 bytes; base-busyread-run.txt on 9f8ad95 under the same conditions: 1,024-byte cut-off comman…staged file: -rw------- vytautas ...; task root: drwx------MODE=busyread live-spawn-real-tmux.sh <base 9f8ad95 extract> base 6(real tmux, secondmate spawn with codex; the pane reads the first typed line, then stays busy)MODE=busyread live-spawn-real-tmux.sh <worktree> target 6live-spawn-real-tmux.sh <worktree|base> <label> 4(pane shell starts after a sleep; base did not reproduce under this condition)PRE=world MODE=busyread live-spawn-real-tmux.sh <worktree> ... 2(world-writable /tmp/fm-<id> planted first)PRE=symlink MODE=busyread live-spawn-real-tmux.sh <worktree> ... 2(/tmp/fm-<id> is a symlink to a world-writable folder)PRE=owned755 MODE=busyread live-spawn-real-tmux.sh <worktree> ... 2TEARDOWN=1 MODE=busyread live-spawn-real-tmux.sh <worktree> ... 2(spawn, thenfm-teardown.sh <id> --force, then check /tmp/fm-<id>)FM_GATE_REFUSE_BYPASS=1 bash tests/fm-kimi-harness.test.sh(changed suite, including the new shared-temp-root test)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.