Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
2e44620
chore(openshell): prepare 0.0.82 dependency upgrade
ericksoa Jul 12, 2026
225e154
fix(mcp): align credential diagnostics with OpenShell main
ericksoa Jul 12, 2026
2bd7c7c
docs(mcp): narrow CONNECT 503 diagnosis
ericksoa Jul 12, 2026
3a67bc3
chore(openshell): prepare 0.0.82 dependency upgrade
ericksoa Jul 12, 2026
9213896
fix(mcp): align credential diagnostics with OpenShell main
ericksoa Jul 12, 2026
1fc5e39
docs(mcp): narrow CONNECT 503 diagnosis
ericksoa Jul 12, 2026
6e90439
feat(exec): support multiline OpenShell commands
ericksoa Jul 13, 2026
a271c66
fix(install): validate OpenShell archives before extraction
ericksoa Jul 13, 2026
06ec576
docs(security): audit OpenShell 0.0.82 migration
ericksoa Jul 13, 2026
18189eb
fix(ci): validate trusted OpenShell release upgrades
ericksoa Jul 12, 2026
12a6b1d
fix(ci): require exact OpenShell release asset sets
ericksoa Jul 12, 2026
ece87f2
fix(ci): bind OpenShell hashes to runtime selectors
ericksoa Jul 12, 2026
36dda9a
docs(security): bind OpenShell dev artifact evidence
ericksoa Jul 13, 2026
5e5fe4b
test(exec): cover carriage-return command arguments
ericksoa Jul 13, 2026
441e0c8
test(install): model archive validation in curl fallback
ericksoa Jul 13, 2026
3658555
docs(security): require trusted upgrade bootstrap
ericksoa Jul 13, 2026
14b7560
refactor(exec): remove obsolete newline transports
ericksoa Jul 13, 2026
20feff9
fix(ci): trust complete OpenShell installer templates
ericksoa Jul 13, 2026
d78caf7
docs(security): correct OpenShell connection test audit
ericksoa Jul 13, 2026
14c246a
test(exec): exercise native multiline transports
ericksoa Jul 13, 2026
ecf1f9f
refactor(exec): remove remaining newline transports
ericksoa Jul 13, 2026
5fe4558
fix(runtime): reject supervisor TLS identity in children
ericksoa Jul 13, 2026
6e27baa
fix(ci): migrate OpenShell version bounds together
ericksoa Jul 13, 2026
b884b7a
chore(openshell): reconcile protected upgrade history
ericksoa Jul 13, 2026
f3833f5
test(install): assert safe archive extraction
ericksoa Jul 13, 2026
3e338a3
test(runtime): isolate supervisor identity assertions
ericksoa Jul 13, 2026
3788f39
test(e2e): refresh exact OpenShell main proof
ericksoa Jul 13, 2026
aef77e1
test(e2e): require the complete MCP agent matrix
ericksoa Jul 13, 2026
ecb9863
test(e2e): prove OpenShell child launch boundaries
ericksoa Jul 13, 2026
a85153f
test(e2e): prove OpenShell runtime contracts
ericksoa Jul 13, 2026
98e7aa2
test(e2e): prove OpenShell credential window
ericksoa Jul 13, 2026
217e440
docs(security): close OpenShell migration audit gaps
ericksoa Jul 13, 2026
d893377
test(e2e): prove OpenShell driver config migration
ericksoa Jul 13, 2026
8f82864
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
2f03477
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
66359f8
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
5074db2
chore(openshell): upgrade stable pin to v0.0.82
ericksoa Jul 13, 2026
7a86d7a
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
5ec6b6d
test(openshell): keep release-data trust extensible
ericksoa Jul 13, 2026
44bce49
test(exec): avoid environment-derived shell path
ericksoa Jul 13, 2026
a765301
test(openshell): keep migration fixtures linear
ericksoa Jul 13, 2026
b016b6d
test(openshell): align upgrade fixtures with 0.0.82
ericksoa Jul 13, 2026
acac53e
ci(e2e): constrain exact-main artifact downloads
ericksoa Jul 13, 2026
6467eb9
test(openshell): keep stale recovery fixture current
ericksoa Jul 13, 2026
a128d8b
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
5b7db6a
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
f7c3ab3
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
acdb3a4
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
ddf425a
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
d6f746e
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
d457b47
test(e2e): prove OpenShell v0.0.82 release
ericksoa Jul 13, 2026
cf92c03
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
f2c8cd1
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
2c4f38e
test(e2e): keep release proof on stable lane
ericksoa Jul 13, 2026
5a67f7f
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
18255c7
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 13, 2026
81895e0
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 14, 2026
7a0d811
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 15, 2026
7cd6a2b
test(e2e): deduplicate OpenShell parity mapping
ericksoa Jul 15, 2026
662638d
Merge remote-tracking branch 'origin/main' into dep/openshell-v0.0.82…
ericksoa Jul 15, 2026
218507a
fix(e2e): shard OpenShell upgrade evidence
ericksoa Jul 15, 2026
33ee348
merge(main): resolve OpenShell 0.0.82 review conflicts
prekshivyas Jul 15, 2026
f13e841
test(openshell): repair 0.0.82 migration gates
prekshivyas Jul 15, 2026
670e777
Merge remote-tracking branch 'origin/main' into codex/pr-6726-fixes
prekshivyas Jul 15, 2026
df76e19
test(e2e): cover immediate OpenShell upgrade predecessor
prekshivyas Jul 15, 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
4 changes: 3 additions & 1 deletion .github/workflows/installer-hash-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
# SPDX-License-Identifier: Apache-2.0
#
# Verifies pinned installer SHA-256 hashes still match upstream scripts.
# Checked: OpenShell v0.0.72 installer and Brev release assets.
# Checked: allowlisted OpenShell installer and Brev release assets.
# Reports the required network-backed drift check on every PR, every push to
# main, and weekly. Pull requests execute checker code from their base commit;
# the immutable bootstrap is used only for the PR that first adds that action.
# A new release-manifest allowlist entry must therefore land on main in a
# prerequisite PR before a later PR changes runtime selectors to that release.

name: Security / Installer Hash Check

Expand Down
2 changes: 1 addition & 1 deletion agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ PY
# file has to also rewrite the Dockerfile-committed hash, which reviewers gate).
# Regenerate with `sha256sum agents/hermes/{hermes-wrapper.py,validate-env-secret-boundary.py,finalize-tirith-marker.py}`.
ARG NEMOCLAW_HERMES_WRAPPER_SHA256=ec8b0e4d6254175929d5a02240acd6af25e94774a30b36ba5c6f5a69c32fb9d7
ARG NEMOCLAW_HERMES_VALIDATOR_SHA256=970d7ff03bc409ff1d5ca46bfdbd2a42ac28a32a810ccc147a508301bff38496
ARG NEMOCLAW_HERMES_VALIDATOR_SHA256=d7371a84099b204346e0bfecfacb0086fdab690e5fadcf5e12417b6038927d19
ARG NEMOCLAW_HERMES_TIRITH_FINALIZER_SHA256=a1e6b1c53ab297569abb87c29d15c294d729e46005bfd022136b4c447a791819
# hadolint ignore=DL4006
RUN printf '%s %s\n' \
Expand Down
4 changes: 3 additions & 1 deletion agents/hermes/mcp-config-transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
ENV_PLACEHOLDER_RE = re.compile(
r"^Bearer openshell:resolve:env:([A-Za-z_][A-Za-z0-9_]{0,127})$"
)
OPENSHELL_REVISIONED_CREDENTIAL_NAME_RE = re.compile(r"^v[0-9]+_[A-Za-z0-9_]+$")
BOUNDARY_MANIFEST_NAME = "openshell-child-visible-credentials.v0.0.72.json"
ANSI_ESCAPE_RE = re.compile(
r"\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|[@-_])"
Expand Down Expand Up @@ -174,7 +175,8 @@ def _manifest_strings(manifest: dict[str, object], key: str) -> frozenset[str]:

def _credential_name_is_reserved(name: str) -> bool:
return (
name in _RAW_CHILD_VALUE_KEYS
OPENSHELL_REVISIONED_CREDENTIAL_NAME_RE.fullmatch(name) is not None
or name in _RAW_CHILD_VALUE_KEYS
or name in _REWRITTEN_CHILD_VALUE_KEYS
or name in _RUNTIME_CONTROL_KEYS
or any(name.startswith(prefix) for prefix in _RUNTIME_CONTROL_PREFIXES)
Expand Down
34 changes: 22 additions & 12 deletions agents/hermes/validate-env-secret-boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@
}
)
RUNTIME_ALLOWED_RAW_SECRET_KEYS = frozenset({"OPENCLAW_GATEWAY_TOKEN"})
# OpenShell's Docker/Podman supervisor owns this variable and injects a mounted
# file path, not private-key material. Keep the allowance exact and runtime-only
# so a caller cannot use the secret-shaped name to smuggle an arbitrary value or
# persist it in Hermes' mutable .env file.
RUNTIME_ALLOWED_PLATFORM_PATH_VALUES = frozenset(
{("OPENSHELL_TLS_KEY", "/etc/openshell/tls/client/tls.key")}
# OpenShell 8eacb477 (candidate 0.0.82) makes these supervisor-only identity
# variables and removes them from entrypoint, exec, and connect children. Their
# presence in Hermes is therefore contract drift even when the value is only a
# mounted path.
OPENSHELL_SUPERVISOR_ONLY_ENV_KEYS = frozenset(
{"OPENSHELL_TLS_CA", "OPENSHELL_TLS_CERT", "OPENSHELL_TLS_KEY"}
)
ALLOWED_LITERALS = frozenset({"", "[STRIPPED_BY_MIGRATION]"})
MAX_ENV_BYTES = 4 * 1024 * 1024
Expand Down Expand Up @@ -451,6 +451,11 @@ def validate_env_file(path: str) -> int:
key = key.strip()
if not KEY_NAME_RE.fullmatch(key):
continue
if key in OPENSHELL_SUPERVISOR_ONLY_ENV_KEYS:
violation_count += 1
if len(violations) < MAX_VIOLATIONS:
violations.append(f"{key} (line {lineno})")
continue
if key in ENV_FILE_ALLOWED_NONSECRET_KEYS:
continue
if key in ENV_FILE_ALLOWED_RAW_SECRET_KEYS and is_allowed_raw_secret_value(
Expand All @@ -468,8 +473,9 @@ def validate_env_file(path: str) -> int:
return 0
_emit_violations(
"[SECURITY] Refusing Hermes startup because /sandbox/.hermes/.env "
"contains raw secret-shaped values. Store credentials in OpenShell "
"providers and keep only openshell resolver placeholders in the sandbox.",
"contains raw secret-shaped values or OpenShell supervisor-only identity "
"variables. Store credentials in OpenShell providers and keep only "
"openshell resolver placeholders in the sandbox.",
violations,
violation_count - len(violations),
)
Expand All @@ -481,14 +487,17 @@ def validate_runtime_env(env: dict[str, str] | None = None) -> int:
violations: list[str] = []
violation_count = 0
for key, value in sorted(source.items()):
if key in OPENSHELL_SUPERVISOR_ONLY_ENV_KEYS:
violation_count += 1
if len(violations) < MAX_VIOLATIONS:
violations.append(key)
continue
if key in RUNTIME_ALLOWED_NONSECRET_KEYS:
continue
if key in RUNTIME_ALLOWED_RAW_SECRET_KEYS and is_allowed_raw_secret_value(
key, value
):
continue
if (key, value) in RUNTIME_ALLOWED_PLATFORM_PATH_VALUES:
continue
if not KEY_NAME_RE.fullmatch(key):
continue
if not SECRET_KEY_RE.search(key):
Expand All @@ -502,8 +511,9 @@ def validate_runtime_env(env: dict[str, str] | None = None) -> int:
return 0
_emit_violations(
"[SECURITY] Refusing Hermes startup because the process environment "
"contains raw secret-shaped values. Store credentials in OpenShell "
"providers and keep only openshell resolver placeholders in the sandbox.",
"contains raw secret-shaped values or OpenShell supervisor-only identity "
"variables. Store credentials in OpenShell providers and keep only "
"openshell resolver placeholders in the sandbox.",
violations,
violation_count - len(violations),
)
Expand Down
29 changes: 17 additions & 12 deletions agents/langchain-deepagents-code/dcode-wrapper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ unset PYTHONHOME PYTHONPATH
readonly DEEPAGENTS_ENV_FILE="/sandbox/.deepagents/.env"
readonly OPENSHELL_ENV_PLACEHOLDER_PREFIX="openshell:resolve:env:"
readonly DEEPAGENTS_CONFIG_FILE="/sandbox/.deepagents/config.toml"
readonly OPENSHELL_TLS_KEY_PATH="/etc/openshell/tls/client/tls.key"
readonly DEEPAGENTS_AUTH_FILE="/sandbox/.deepagents/.state/auth.json"
readonly DEEPAGENTS_CODEX_AUTH_FILE="/sandbox/.deepagents/.state/chatgpt-auth.json"
readonly MANAGED_DCODE_AUTO_APPROVAL_FILE="/usr/local/share/nemoclaw/dcode-auto-approval"
Expand Down Expand Up @@ -351,14 +350,15 @@ has_credential_name_context() {
return 1
}

# SECURITY: OpenShell's supervisor injects this mounted TLS key path into the
# runtime environment. Allow only the exact name/value pair after the generic
# value scan. Never allow the name alone, and never apply this exception to the
# mutable Deep Agents Code .env file.
is_allowed_openshell_runtime_value() {
local name="$1"
local value="$2"
[ "$name" = "OPENSHELL_TLS_KEY" ] && [ "$value" = "$OPENSHELL_TLS_KEY_PATH" ]
# OpenShell 8eacb477 (candidate 0.0.82) strips these supervisor identity
# variables from entrypoint, exec, and connect children. Reject their presence
# regardless of value so a runtime regression cannot silently expose mounted
# mTLS identity to dcode.
is_openshell_supervisor_only_env_name() {
case "$1" in
OPENSHELL_TLS_CA | OPENSHELL_TLS_CERT | OPENSHELL_TLS_KEY) return 0 ;;
esac
return 1
}

# OTLP endpoint variables carry the collector URL, not a credential. The
Expand Down Expand Up @@ -440,8 +440,7 @@ is_openshell_env_placeholder_for_name() {
local canonical revision_prefix revision_suffix versioned revision

# OPENSHELL_TLS_KEY is supervisor infrastructure, not a provider credential.
# Only its exact mounted path is accepted from the runtime environment below;
# never let a provider placeholder bypass that name/value allowlist.
# Never let a provider placeholder bypass that supervisor-only boundary.
[ "$name" != "OPENSHELL_TLS_KEY" ] || return 1

# Keep this identifier contract aligned with OpenShell provider env keys.
Expand Down Expand Up @@ -506,6 +505,9 @@ assert_no_secret_runtime_env() {
name="${pair%%=*}"
[ "$name" != "$pair" ] || continue
value="${pair#*=}"
if is_openshell_supervisor_only_env_name "$name"; then
refuse_secret_env "runtime environment variable" "$name"
fi
if [[ "$value" == *"$OPENSHELL_ENV_PLACEHOLDER_PREFIX"* ]]; then
if is_openshell_env_placeholder_for_name "$name" "$value"; then
continue
Expand All @@ -526,7 +528,7 @@ assert_no_secret_runtime_env() {
fi
continue
fi
if has_credential_name_context "$name" && [ ${#value} -ge 10 ] && ! is_allowed_openshell_runtime_value "$name" "$value"; then
if has_credential_name_context "$name" && [ ${#value} -ge 10 ]; then
refuse_secret_env "runtime environment variable" "$name"
fi
done < <(env -0)
Expand Down Expand Up @@ -578,6 +580,9 @@ assert_no_secret_env_file() {
;;
esac
value="$(trim_whitespace "$value")"
if is_openshell_supervisor_only_env_name "$key"; then
refuse_secret_env "$env_file" "$key"
fi
if is_dynamic_dotenv_value "$value"; then
refuse_dynamic_env "$env_file" "$key"
fi
Expand Down
12 changes: 10 additions & 2 deletions agents/langchain-deepagents-code/managed-dcode-runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@
r"\u202f\u205f\u3000\ufeff'\"]"
)
_OPENSHELL_ENV_PLACEHOLDER_PREFIX = "openshell:resolve:env:"
# OpenShell 8eacb477 reserves these for the supervisor and strips them from all
# child process shapes; fail closed if a regressed runtime exposes them here.
_OPENSHELL_SUPERVISOR_ONLY_ENV_NAMES = frozenset(
{"OPENSHELL_TLS_CA", "OPENSHELL_TLS_CERT", "OPENSHELL_TLS_KEY"}
)
_UPSTREAM_PROVIDER_ENV = "NEMOCLAW_UPSTREAM_PROVIDER"
_FETCH_URL_TRUSTED_PROXY_ENV = (
"DEEPAGENTS_CODE_FETCH_URL_TRUSTED_PROXY_URL"
Expand Down Expand Up @@ -236,8 +241,6 @@ def _is_openshell_placeholder_for_name(name: str, value: str) -> bool:
def _is_managed_value(name: str, value: str) -> bool:
if name == "DEEPAGENTS_CODE_OPENAI_API_KEY":
return value == "nemoclaw-managed-inference"
if name == "OPENSHELL_TLS_KEY":
return value == "/etc/openshell/tls/client/tls.key"
if name == "SLACK_BOT_TOKEN":
return bool(re.fullmatch(r"xoxb-[A-Za-z0-9_-]{10,}", value)) and not _contains_other_platform_secret(value, "slack")
if name == "SLACK_APP_TOKEN":
Expand Down Expand Up @@ -283,6 +286,11 @@ def _is_safe_otlp_endpoint_url(value: str) -> bool:

def _assert_safe_environment() -> None:
for name, value in os.environ.items():
if name in _OPENSHELL_SUPERVISOR_ONLY_ENV_NAMES:
raise RuntimeError(
f"runtime environment variable {name} is reserved for the "
"OpenShell supervisor and must not reach a child process"
)
if _OPENSHELL_ENV_PLACEHOLDER_PREFIX in value:
if _is_openshell_placeholder_for_name(name, value):
continue
Expand Down
4 changes: 4 additions & 0 deletions docs/deployment/set-up-mcp-bridge.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ The child-visible compatibility list is pinned to OpenShell `v0.0.72` commit `8c
Choose a dedicated name such as `MY_SERVICE_MCP_TOKEN`.
NemoClaw also rejects host subprocess control names such as `PATH`, proxy/TLS variables, and `OPENSHELL_*`, `GRPC_*`, `LC_*`, or `XDG_*` keys so the selected credential cannot be inherited by unrelated OpenShell commands.
Loader, shell, language, and agent runtime controls such as `LD_PRELOAD`, `BASH_ENV`, `NODE_OPTIONS`, `PYTHONHOME`, `NEMOCLAW_*`, and `OPENCLAW_*` are rejected as well because OpenShell attaches provider keys to fresh sandbox execs; use a dedicated service name such as `MY_SERVICE_MCP_TOKEN`.
OpenShell reserves credential names matching `v[0-9]+_[A-Za-z0-9_]+` for revisioned placeholders.
NemoClaw rejects names such as `v10_GITHUB_TOKEN` because OpenShell skips them instead of attaching a credential resolver.

NemoClaw requires exactly one `--env` bearer credential per server.
Every endpoint must use HTTPS.
Expand Down Expand Up @@ -323,6 +325,8 @@ For an identical HTTP 401 or 403, a confirmed-valid credential settles it: the O
An identical HTTP 400 stays inconclusive even with a valid credential, because the endpoint may reject the probe's `initialize` request itself; compare `mcp status` for the same server on a known-good host — if that host verifies, suspect this host's placeholder rewrite.
That is a host-side OpenShell defect rather than a NemoClaw registration problem: verify the OpenShell installation on the host (tracked upstream as OpenShell issue 2161).
A `credential resolution: unknown` verdict with an endpoint or policy detail means the probe could not reach a judgment; fix the reported endpoint or policy condition and rerun `mcp status <server>`.
A detail containing `CONNECT 503` means OpenShell failed closed before TLS setup because gateway TLS termination state was unavailable.
Inspect the OpenShell gateway's ephemeral CA generation and CA-file initialization, repair the reported condition, and rerun `mcp status <server>`.

If `restart` reports a missing provider and the original credential is not registered in OpenShell, export the same variable name used during `add` and retry.

Expand Down
30 changes: 20 additions & 10 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -894,11 +894,16 @@ printf 'hello\n' | $$nemoclaw my-assistant exec --stdin -- cat
ssh dgx-spark '$$nemoclaw my-assistant exec --no-stdin -- pwd'
```

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 --stdin -- bash`).
Or write the script to a file in the sandbox and run it (`$$nemoclaw <name> exec -- bash <script-path>`).
OpenShell preserves line endings and quote characters inside each command argument, so inline scripts and heredocs can be passed as one argument after `--`.
For example, a shell variable keeps the multi-line script in one argv element:

```bash
script=$'cat <<\'EOF\'\nline one\nline two\nEOF'
$$nemoclaw <name> exec -- bash -lc "$script"
```

NUL bytes are still rejected in command arguments.
Line breaks are accepted only in command argv: `--workdir` remains single-line, and NemoClaw does not expose OpenShell request-environment injection on this command.

| Flag | Description |
|------|-------------|
Expand Down Expand Up @@ -1367,11 +1372,16 @@ By default, NemoClaw inherits caller stdin only when it is a terminal.
Non-terminal or unavailable stdin is closed so SSH, CI, and other one-shot commands cannot wait on an inherited pipe.
Pass `--stdin` to forward an intentional pipe, or `--no-stdin` to close terminal stdin explicitly.

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 --stdin -- bash`).
Or write the script to a file in the sandbox and run it (`$$nemoclaw <name> exec -- bash <script-path>`).
OpenShell preserves line endings and quote characters inside each command argument, so inline scripts and heredocs can be passed as one argument after `--`.
For example, a shell variable keeps the multi-line script in one argv element:

```bash
script=$'cat <<\'EOF\'\nline one\nline two\nEOF'
$$nemoclaw <name> exec -- bash -lc "$script"
```

NUL bytes are still rejected in command arguments.
Line breaks are accepted only in command argv: `--workdir` remains single-line, and NemoClaw does not expose OpenShell request-environment injection on this command.

| Flag | Description |
|------|-------------|
Expand Down
Loading
Loading