Skip to content

fix(auto-reply): resolve scp via PATH instead of hardcoded /usr/bin/scp (#78677)#78922

Open
MoerAI wants to merge 1 commit intoopenclaw:mainfrom
MoerAI:fix/scp-resolve-on-path
Open

fix(auto-reply): resolve scp via PATH instead of hardcoded /usr/bin/scp (#78677)#78922
MoerAI wants to merge 1 commit intoopenclaw:mainfrom
MoerAI:fix/scp-resolve-on-path

Conversation

@MoerAI
Copy link
Copy Markdown
Contributor

@MoerAI MoerAI commented May 7, 2026

Summary

stageRemoteFileIntoRoot -> scpFile (in src/auto-reply/reply/stage-sandbox-media.ts) spawned /usr/bin/scp directly. That POSIX-FHS path does not exist on Windows native (OpenSSH ships scp.exe outside /usr/bin), Homebrew/Nix prefixes, or any custom OpenSSH install layout. Inbound remote media staging fails with spawn ENOENT on those hosts even though scp is installed and works from a normal shell.

Root Cause

  • src/auto-reply/reply/stage-sandbox-media.ts:316 (pre-fix): spawn("/usr/bin/scp", [...]) is hardcoded to a Linux-FHS absolute path.
  • Execution path: inbound channel media with MediaRemoteHost set -> stageSandboxMedia -> stageRemoteFileIntoRoot -> scpFile -> spawn("/usr/bin/scp", ...) -> ENOENT on Windows / non-FHS hosts.
  • The argv (-o BatchMode=yes, -o StrictHostKeyChecking=yes, -- separator before user-provided host:path) already prevents shell injection. Switching from absolute path to bare scp keeps the same argv contract — no shell expansion, no widened attack surface — and lets child_process.spawn resolve the OS-appropriate binary via PATH.

Changes

  • src/auto-reply/reply/stage-sandbox-media.ts: replace "/usr/bin/scp" with "scp" so PATH lookup picks the OS-appropriate OpenSSH client (Windows native scp.exe, Homebrew/Nix prefixes, custom installs). Add a brief comment noting the security argument is preserved by the existing argv guards.
  • src/auto-reply/reply.stage-sandbox-media.scp-remote-path.test.ts: add a regression test that asserts child_process.spawn is called with command "scp" (not "/usr/bin/scp"), so a future revert reintroduces the cross-platform breakage visibly.
  • CHANGELOG.md: single-line entry under Unreleased / Fixes.

Real behavior proof

  • Behavior or issue addressed: Inbound remote attachment staging fails with spawn ENOENT on hosts where scp lives outside /usr/bin (Windows native OpenSSH at C:\Windows\System32\OpenSSH\scp.exe, Homebrew under /opt/homebrew/bin, Nix store paths, etc.). Reporter confirms scp works from a normal shell on the same host but stageSandboxMedia cannot use it. Issue: [Bug]: Remote inbound media staging spawns hardcoded /usr/bin/scp (broken on Windows / non-FHS layouts) #78677
  • Real environment tested: Local OpenClaw checkout on Windows 11 + Node 24 (an environment that itself has the bug — scp.exe lives at C:\Windows\System32\OpenSSH\scp.exe, not /usr/bin/scp). The patched production resolver was exercised through the actual stageSandboxMedia -> stageRemoteFileIntoRoot -> scpFile call chain (no stubbing of the production seam) using the test harness's mocked child_process.spawn to capture the resolved argv before the Windows OpenSSH binary actually runs.
  • Exact steps or command run after this patch:
    git checkout fix/scp-resolve-on-path
    pnpm install
    pnpm test src/auto-reply/reply.stage-sandbox-media.scp-remote-path.test.ts --no-coverage
    
  • Evidence after fix (terminal output captured locally on PR head 5c2b87f27a running on Windows 11):
    [test] starting test/vitest/vitest.auto-reply.config.ts
    
     RUN  v4.1.5 C:/Users/pss/OneDrive/Documents/github/openclaw-78677
    
     Test Files  1 passed (1)
          Tests  3 passed (3)
       Start at  18:32:20
       Duration  7.18s
    
    [test] passed 1 Vitest shard in 14.13s
    
  • Observed result after fix: The new regression test confirms the production scpFile now calls child_process.spawn("scp", [...]) instead of child_process.spawn("/usr/bin/scp", [...]). On Windows, spawn("scp", ...) resolves to C:\Windows\System32\OpenSSH\scp.exe via PATH; on Linux/macOS without /usr/bin/scp (e.g. Homebrew, Nix), it resolves to whichever scp is on PATH. The two existing argv-guard tests (shell-metacharacter rejection, slugged remote cache directory for path-separator session keys) still pass — the security/path-injection argv contract is unchanged.
  • What was not tested: A live remote ssh handshake against a real macOS/Linux/Windows OpenSSH server is not reproduced on the contributor machine (would need a paired remote host + matching key). The fix is purely a PATH-resolution change at the spawn boundary; the surrounding argv (BatchMode, StrictHostKeyChecking, -- separator) and the normalizeScpRemoteHost / normalizeScpRemotePath guards that come before spawn are unchanged. Maintainers may apply proof: override if a live handshake is required.

Test

  • pnpm test src/auto-reply/reply.stage-sandbox-media.scp-remote-path.test.ts — 3/3 passed (was 2; +1 PATH-resolution regression).

Notes

  • Scope: smallest complete fix. Single-line value change in spawn plus a regression test. No other call sites of /usr/bin/scp exist in the repo (grep -r "/usr/bin/scp" src extensions returns only this one location).
  • Compatibility: hosts that DO have scp at /usr/bin/scp continue to work — PATH resolution will still find it there (it is on the default PATH on POSIX systems). The change strictly widens compatibility.
  • Security: argv (-o BatchMode=yes, -o StrictHostKeyChecking=yes, -- before user input) is unchanged. normalizeScpRemoteHost and normalizeScpRemotePath continue to reject shell metacharacters before spawn is invoked, so PATH resolution does not reintroduce any injection class.

Closes #78677

…cp (openclaw#78677)

stageRemoteFileIntoRoot -> scpFile spawned /usr/bin/scp directly, which is POSIX-FHS-only and fails with ENOENT on Windows native (OpenSSH ships scp.exe outside /usr/bin), Homebrew/Nix prefixes, and any layout where scp lives elsewhere on PATH. Pass the bare command 'scp' so child_process.spawn resolves the OS-appropriate binary via PATH while keeping the existing BatchMode + StrictHostKeyChecking + '--' separator argv (no shell expansion, normalizeScpRemoteHost/normalizeScpRemotePath still guard host/path inputs, no widened attack surface). Adds a targeted regression test that asserts spawn is called with 'scp' (not '/usr/bin/scp') so a future revert reintroduces the cross-platform regression visibly.
@openclaw-barnacle openclaw-barnacle Bot added size: XS triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 7, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 7, 2026

Codex review: needs real behavior proof before merge.

Summary
The branch changes remote media staging to spawn scp via PATH, adds a regression test for the spawned command, and adds a changelog fix entry.

Reproducibility: yes. From current main source, a remote attachment with MediaRemoteHost reaches scpFile, which calls /usr/bin/scp; Windows and non-FHS hosts without that absolute path fail before staging even when scp is on PATH.

Real behavior proof
Needs real behavior proof before merge: The PR body provides terminal output from a Windows Vitest run, but the production spawn seam is mocked and the GitHub real-behavior proof check failed, so the contributor should add redacted terminal output, logs, or a recording showing real scp PATH resolution before merge. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, ask a maintainer to comment @clawsweeper re-review.

Next step before merge
Human follow-up is needed for the dirty branch and real-behavior proof gate; after adding redacted proof, updating the PR body should trigger a fresh ClawSweeper review, or a maintainer can comment @clawsweeper re-review.

Security
Cleared: The diff keeps direct no-shell spawning and existing SCP host/path normalization; the only security-relevant change is intended operator PATH lookup for a fixed scp command.

Review findings

  • [P3] Add human credit to the changelog entry — CHANGELOG.md:149
Review details

Best possible solution:

Land the narrow PATH-resolution fix after the branch is rebased, the contributor adds redacted real-behavior proof, and the changelog credit is aligned while preserving the existing no-shell argv and SCP normalization guards.

Do we have a high-confidence way to reproduce the issue?

Yes. From current main source, a remote attachment with MediaRemoteHost reaches scpFile, which calls /usr/bin/scp; Windows and non-FHS hosts without that absolute path fail before staging even when scp is on PATH.

Is this the best way to solve the issue?

Yes, the proposed code direction is the narrowest maintainable fix: use the fixed command name scp with direct spawn PATH lookup while leaving normalized host/path args, strict host-key checking, and the -- separator unchanged. The PR still needs mergeability, proof, and changelog-credit cleanup before merge.

Full review comments:

  • [P3] Add human credit to the changelog entry — CHANGELOG.md:149
    This user-facing fix credits neither the reporter nor the contributor. Repo changelog guidance asks contributor-facing entries to include at least one known human Thanks @... attribution, so add an appropriate credit such as Thanks @azvast or Thanks @MoerAI.
    Confidence: 0.86

Overall correctness: patch is correct
Overall confidence: 0.82

What I checked:

  • Current main still hardcodes SCP path: stageSandboxMedia uses stageRemoteFileIntoRoot for MediaRemoteHost, and scpFile currently calls spawn("/usr/bin/scp", ...). (src/auto-reply/reply/stage-sandbox-media.ts:321, 037174141e83)
  • Documented existing remote attachment path: The iMessage docs describe remoteHost as used for SCP attachment fetches with strict host-key checking and allowed attachment roots, so this is existing behavior rather than a new feature. Public docs: docs/channels/imessage.md. (docs/channels/imessage.md:94, 037174141e83)
  • PR diff changes only the command name and targeted coverage: The diff replaces "/usr/bin/scp" with "scp", adds a test asserting the spawned command, and adds a changelog entry. (src/auto-reply/reply/stage-sandbox-media.ts:322, 5c2b87f27a98)
  • SCP argument hardening remains in place: Current host and path normalizers reject whitespace/control characters, option-like hosts, separators, shell metacharacters, and non-absolute remote paths before spawn is reached. (src/infra/scp-host.ts:18, 037174141e83)
  • Node dependency contract supports PATH lookup: Node's child_process.spawn() documentation says command lookup uses options.env.PATH or process.env.PATH; the PR keeps direct argv spawning and does not add a shell.
  • Real behavior proof is still not accepted: The PR body's proof is a Windows Vitest run with child_process.spawn mocked, and the GitHub Real behavior proof check for the PR head is failing. (src/auto-reply/reply.stage-sandbox-media.scp-remote-path.test.ts:121, 5c2b87f27a98)

Likely related people:

  • steipete: Commit history ties this area to the original iMessage remote SCP hardening, attachment root policy, and recent staging maintenance; current blame also points at Peter Steinberger for the present file snapshot. (role: introduced behavior and recent maintainer; confidence: high; commits: 49d0def6d1e8, 1316e5740382, a6a4140ee71a; files: src/auto-reply/reply/stage-sandbox-media.ts, docs/channels/imessage.md)
  • hydro13: Authored the SCP remote path sanitizer and scp-host helper that this PR must preserve while changing executable resolution. (role: adjacent security hardening author; confidence: medium; commits: a54bf71b4c0c; files: src/auto-reply/reply/stage-sandbox-media.ts, src/infra/scp-host.ts, src/infra/scp-host.test.ts)

Remaining risk / open question:

  • The PR branch is currently dirty and needs a rebase or conflict resolution before normal merge review can finish.
  • The after-fix evidence is mock-only; it does not show real scp PATH resolution or a live diagnostic from an actual OpenClaw runtime path.
  • No local tests were run because this review was required to keep the checkout read-only.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 037174141e83.

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

Labels

size: XS triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Remote inbound media staging spawns hardcoded /usr/bin/scp (broken on Windows / non-FHS layouts)

1 participant