Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3fa8319
fix(exec): reject multi-line exec args with recovery guidance
yimoj Jun 29, 2026
845c7d0
docs(commands): note exec rejects multi-line arguments
yimoj Jun 29, 2026
3594786
fix(exec): redact multi-line arg preview and add pipe workaround
yimoj Jun 29, 2026
78dde86
test(exec): make multi-line guard tests CI-safe
yimoj Jun 29, 2026
045b3df
test(exec): add command-level coverage for the multi-line scenarios
yimoj Jun 29, 2026
929ca22
refactor(exec): injectable seams for hermetic dispatch and workdir tests
yimoj Jun 30, 2026
9fbca45
test(exec): cover stdin-pipe/script-file workarounds and guard ordering
yimoj Jun 30, 2026
6dc6d82
test(exec): assert default runner inherits stdio; document guard sour…
yimoj Jun 30, 2026
2d3c52c
docs(exec): document why the multi-line guard matches only CR/LF
yimoj Jun 30, 2026
050123d
test(exec): cover Unicode line separators; explain line-count logic
yimoj Jun 30, 2026
dea380a
test(exec): split multi-line guard suites into own file; mirror semic…
yimoj Jun 30, 2026
d5e34ae
test(exec): align command-layer semicolon test name with action layer
yimoj Jun 30, 2026
8174e22
test(exec): cover Unicode separator at dispatch and trailing-newline …
yimoj Jun 30, 2026
4277fe7
merge(cli): resolve main conflicts for exec multiline guard (#5991)
cjagwani Jul 1, 2026
3dc0ff2
Merge branch 'main' into fix/5980-exec-newline-guidance
cjagwani Jul 1, 2026
938a5ac
docs(exec): reference upstream OpenShell#2110 as the multiline-guard …
cjagwani Jul 1, 2026
3f88185
Merge branch 'main' into fix/5980-exec-newline-guidance
cjagwani Jul 1, 2026
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
6 changes: 6 additions & 0 deletions docs/reference/commands-nemohermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,12 @@ Everything after `--` is forwarded verbatim to the sandbox command, including fl

The exit code is the remote command's exit code.

The OpenShell exec endpoint rejects any command argument (the values after `--`) that contains a newline or carriage return, so multi-line commands such as a `bash` heredoc cannot be passed through `exec`.
NemoClaw detects this before dispatch, names the offending argument position, and exits with status `2` instead of surfacing the lower-level OpenShell `InvalidArgument` error.
Join the statements with semicolons (`nemohermes <name> exec -- bash -lc "cmd1; cmd2"`).
Pipe the script into the sandbox shell over stdin (`printf 'cmd1\ncmd2\n' | nemohermes <name> exec -- bash`).
Or write the script to a file in the sandbox and run it (`nemohermes <name> exec -- bash <script-path>`).

| Flag | Description |
|------|-------------|
| `--workdir <dir>` | Working directory inside the sandbox. The directory is checked before the command runs; if it does not exist, NemoClaw reports `error: --workdir: <dir> does not exist inside the sandbox` and exits with status `1` without invoking the inner command. |
Expand Down
6 changes: 6 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,12 @@ The exit code is the remote command's exit code.

</AgentOnly>

The OpenShell exec endpoint rejects any command argument (the values after `--`) that contains a newline or carriage return, so multi-line commands such as a `bash` heredoc cannot be passed through `exec`.
NemoClaw detects this before dispatch, names the offending argument position, and exits with status `2` instead of surfacing the lower-level OpenShell `InvalidArgument` error.
Join the statements with semicolons (`$$nemoclaw <name> exec -- bash -lc "cmd1; cmd2"`).
Pipe the script into the sandbox shell over stdin (`printf 'cmd1\ncmd2\n' | $$nemoclaw <name> exec -- bash`).
Or write the script to a file in the sandbox and run it (`$$nemoclaw <name> exec -- bash <script-path>`).

| Flag | Description |
|------|-------------|
| `--workdir <dir>` | Working directory inside the sandbox. The directory is checked before the command runs; if it does not exist, NemoClaw reports `error: --workdir: <dir> does not exist inside the sandbox` and exits with status `1` without invoking the inner command. |
Expand Down
37 changes: 37 additions & 0 deletions src/commands/sandbox/exec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,43 @@ describe("SandboxExecCommand oclif parse path", () => {
});
});

it("forwards a multi-line heredoc command verbatim to the action guard (#5980)", async () => {
// The command layer forwards argv unchanged; execSandbox() applies the
// newline guard (exit 2 before dispatch), which is asserted directly in the
// action test. Here we pin that the heredoc reaches the action intact.
const heredoc = "cat <<EOF\nline1\nline2\nEOF";
await SandboxExecCommand.run(["alpha", "--", "bash", "-lc", heredoc], rootDir);
expect(execSandboxMock).toHaveBeenCalledWith("alpha", ["bash", "-lc", heredoc], {
workdir: undefined,
tty: null,
timeoutSeconds: undefined,
});
});

it("forwards the semicolon workaround to dispatch (#5980)", async () => {
// Mirrors the action-layer "forwards the semicolon workaround to dispatch"
// test: the single-line semicolon-joined command carries no newline, so the
// command layer hands it to execSandbox() unchanged, which then dispatches.
await SandboxExecCommand.run(["alpha", "--", "bash", "-lc", "echo line1; echo line2"], rootDir);
expect(execSandboxMock).toHaveBeenCalledWith(
"alpha",
["bash", "-lc", "echo line1; echo line2"],
{ workdir: undefined, tty: null, timeoutSeconds: undefined },
);
});

it("preserves --workdir and forwards a single-line command unchanged (#5980)", async () => {
await SandboxExecCommand.run(
["alpha", "--workdir", "/sandbox", "--", "bash", "-lc", "echo line1; echo line2"],
rootDir,
);
expect(execSandboxMock).toHaveBeenCalledWith(
"alpha",
["bash", "-lc", "echo line1; echo line2"],
{ workdir: "/sandbox", tty: null, timeoutSeconds: undefined },
);
});

it("parses --tty / --no-tty and --timeout into typed options", async () => {
await SandboxExecCommand.run(["alpha", "--tty", "--timeout", "30", "--", "hostname"], rootDir);
expect(execSandboxMock).toHaveBeenCalledWith("alpha", ["hostname"], {
Expand Down
Loading
Loading