#Use secret refs for sandbox.docker.env
34 messages · Page 1 of 1 (latest)
Short answer: not as a first-class feature today.
I checked the current docs/source, and agents.defaults.sandbox.docker.env is not on the supported SecretRef surface, so the secrets system does not currently manage that field the same way it does provider API keys, gateway auth, or sandbox SSH material.
Docs:
- Secrets: https://docs.openclaw.ai/gateway/secrets
- Supported SecretRef surface: https://docs.openclaw.ai/reference/secretref-credential-surface
- Sandboxing: https://docs.openclaw.ai/gateway/sandboxing
So the practical workarounds are:
- Best if possible: avoid putting the Linear key in
sandbox.docker.envat all, and run that integration on the host side where a SecretRef-supported field exists. - If it must stay sandboxed: keep the secret outside
openclaw.jsonin a separate secret store/file, then mount it read-only into the sandbox or use a custom sandbox image/entrypoint that reads it at runtime.
I would not rely on "${LINEAR_API_KEY}" inside sandbox.docker.env as a supported SecretRef pattern here.
If you want, I can sketch a minimal read-only bind or custom-image setup for Linear.
Yes let's do this
Yep, the least ugly workaround I’d use is:
mount the secret as a read-only file, then have the sandbox load it from /etc/profile.d instead of storing it in sandbox.docker.env.
1) Put the Linear key in a host file, outside your repo
Use a dedicated path, not ~/.ssh / ~/.config style dirs.
sudo install -d -m 700 /opt/openclaw-secrets
printf '%s' 'lin_api_xxx' | sudo tee /opt/openclaw-secrets/linear_api_key >/dev/null
sudo chmod 600 /opt/openclaw-secrets/linear_api_key
2) Add this to openclaw.json (JSON5)
{
agents: {
defaults: {
sandbox: {
docker: {
binds: [
"/opt/openclaw-secrets/linear_api_key:/run/secrets/linear_api_key:ro",
],
readOnlyRoot: false,
setupCommand: [
"mkdir -p /etc/profile.d",
"cat >/etc/profile.d/linear.sh <<'EOF'",
"if [ -f /run/secrets/linear_api_key ]; then",
" export LINEAR_API_KEY=\"$(cat /run/secrets/linear_api_key)\"",
"fi",
"EOF",
"chmod 0644 /etc/profile.d/linear.sh"
]
}
}
}
}
}
Why this works:
- the secret is not in
openclaw.json - it is mounted read-only
- OpenClaw sandbox execs use a login shell, so
/etc/profile.d/linear.shgets loaded on future turns
3) Recreate the sandbox so setupCommand runs again
openclaw sandbox recreate --all
Docs:
- Sandboxing: https://docs.openclaw.ai/gateway/sandboxing
- Sandbox CLI: https://docs.openclaw.ai/cli/sandbox
Small caveat
If your sandbox image is non-root, setupCommand may hit a permission error writing /etc/profile.d. If that happens, the cleaner long-term version is a custom sandbox image with that linear.sh file baked in.
If you want, I can also give you the custom image version, which is cleaner than readOnlyRoot: false.
agents: {
defaults: {
sandbox: {
docker: {
binds: [
"/opt/openclaw-secrets/linear_api_key:/run/secrets/linear_api_key:ro",
],
readOnlyRoot: false,
setupCommand: [
"mkdir -p /etc/profile.d",
"cat >/etc/profile.d/linear.sh <<'EOF'",
"if [ -f /run/secrets/linear_api_key ]; then",
" export LINEAR_API_KEY="$(cat /run/secrets/linear_api_key)"",
"fi",
"EOF",
"chmod 0644 /etc/profile.d/linear.sh"
]
}
}
}
}
}
can't I just have a setup command that directly export using cat?
Not quite.
setupCommand runs once when the sandbox container is created. So if you do:
export LINEAR_API_KEY="$(cat /run/secrets/linear_api_key)"
that only affects that one setup shell. Later OpenClaw turns run fresh shells inside the container, so they will not inherit that export.
So the rule is:
- direct
exportinsetupCommand→ no, not persistent - write a file that future login shells source (
/etc/profile.d/...) → yes - start one long-lived service from
setupCommandwith the env inline → yes, but only for that service
For example, this works only if you launch the actual process there:
LINEAR_API_KEY="$(cat /run/secrets/linear_api_key)" your-command-here
If you want the env available for normal future sandbox execs, the /etc/profile.d/linear.sh trick is the right one.
If your Linear tool supports something like LINEAR_API_KEY_FILE, that’s even cleaner, because then you can just mount the file and skip the export entirely.
/.openclaw/workspace). Use a dangerous override only when you fully trust this runtime.
Apr 15 17:20:20 ip-172-26-7-36 node[364271]: 2026-04-15T17:20:20.470+00:00 [diagnostic] lane task error: lane=main durationMs=2016 error="Error: Sandbox security: bind mount "/home/ubuntu/linear_api_key:/run/secrets/linear_api_key:ro" source "/home/ubuntu/linear_api_key" is outside allowed roots (/home/ubuntu/.openclaw/sandboxes/agent-main-slack-channel-c0aszs4-ea97881d, /home/ubuntu/.openclaw/workspace). Use a dangerous override only when you fully trust this runtime."
Yep, that error is the sandbox guardrail doing its job.
By default, bind sources are only allowed from the workspace / agent workspace roots. Your file is in /home/ubuntu/linear_api_key, so it gets rejected as an external bind source.
If this is your own trusted runtime, the fix is:
agents: {
defaults: {
sandbox: {
docker: {
binds: [
"/home/ubuntu/linear_api_key:/run/secrets/linear_api_key:ro",
],
dangerouslyAllowExternalBindSources: true,
readOnlyRoot: false,
setupCommand: [
"mkdir -p /etc/profile.d",
"cat >/etc/profile.d/linear.sh <<'EOF'",
"if [ -f /run/secrets/linear_api_key ]; then",
" export LINEAR_API_KEY=\"$(cat /run/secrets/linear_api_key)\"",
"fi",
"EOF",
"chmod 0644 /etc/profile.d/linear.sh"
]
}
}
}
}
Then recreate the sandbox:
openclaw sandbox recreate --all
Docs: https://docs.openclaw.ai/gateway/sandboxing
My recommendation:
- If this is a personal/private agent: this override is reasonable.
- If this agent handles untrusted stuff: better move the secret file under an allowed root instead, rather than opening external bind sources globally.
If you want, I can show the per-agent version so you don’t enable that override for every sandbox.
the let's create linear_api_key inside the agent workspace? what would be the new bind?
Yep, use the stable workspace root, not the transient ~/.openclaw/sandboxes/agent-* path.
I’d put it here:
/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key
Then the bind becomes:
binds: [
"/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro",
]
So your Docker block can be:
docker: {
binds: [
"/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro",
],
readOnlyRoot: false,
setupCommand: [
"mkdir -p /etc/profile.d",
"cat >/etc/profile.d/linear.sh <<'EOF'",
"if [ -f /run/secrets/linear_api_key ]; then",
" export LINEAR_API_KEY=\"$(cat /run/secrets/linear_api_key)\"",
"fi",
"EOF",
"chmod 0644 /etc/profile.d/linear.sh"
]
}
Nice part: with that path, you should not need dangerouslyAllowExternalBindSources: true.
I would also make the host file private:
mkdir -p /home/ubuntu/.openclaw/workspace/.secrets
chmod 700 /home/ubuntu/.openclaw/workspace/.secrets
printf '%s' 'lin_api_xxx' > /home/ubuntu/.openclaw/workspace/.secrets/linear_api_key
chmod 600 /home/ubuntu/.openclaw/workspace/.secrets/linear_api_key
Then:
openclaw sandbox recreate --all
If this fixes it, please right-click the solution message → Apps → ✅ Mark Solution so Answer Overflow can index it.
ane=main durationMs=19370 error="Error: /bin/sh: 2: cannot create /etc/profile.d/linear.sh: Read-only file system
Apr 15 17:28:52 ip-172-26-7-36 node[364271]: chmod: cannot access '/etc/profile.d/linear.sh': No such file or directory"
Ah, nice catch. /etc/profile.d is on the container rootfs, so if that stays read-only, write it into the writable sandbox home/workdir instead.
OpenClaw sandbox execs set HOME=/workspace and run a login shell, so use:
docker: {
binds: [
"/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro",
],
setupCommand: [
"cat >/workspace/.profile <<'EOF'",
"if [ -f /run/secrets/linear_api_key ]; then",
" export LINEAR_API_KEY=\"$(cat /run/secrets/linear_api_key)\"",
"fi",
"EOF",
"chmod 0600 /workspace/.profile"
]
}
So yes:
- new bind stays the same
- just change the target file from
/etc/profile.d/linear.shto/workspace/.profile - you can now remove
readOnlyRoot: false
Then recreate the sandbox:
openclaw sandbox recreate --all
If you already have something in /workspace/.profile, use >> instead of > so you append instead of overwrite.
"sandbox": { "docker": { "binds": ["/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro",], "env": {}, "image": "openclaw-sandbox-common:bookworm-slim", "network": "bridge", "user": "1000:1000", "setupCommand": [ "mkdir -p /etc/profile.d", "cat >/etc/profile.d/linear.sh <<'EOF'", "if [ -f /run/secrets/linear_api_key ]; then", " export LINEAR_API_KEY="$(cat /run/secrets/linear_api_key)"", "fi", "EOF", "chmod 0644 /etc/profile.d/linear.sh" ] }, "mode": "all", "scope": "session", "workspaceAccess": "ro" }
this is my sandbox config btw
Yep, with that config the problem is clear:
user: "1000:1000"means the setup runs as a non-root user- you’re trying to write to
/etc/profile.d - rootfs is effectively read-only for that path
So /etc/profile.d is the wrong target here.
With workspaceAccess: "ro", the writable sandbox workspace is still /workspace, and the agent workspace is mounted read-only at /agent. So change only the setupCommand to write /workspace/.profile instead:
"sandbox": {
"docker": {
"binds": [
"/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro"
],
"env": {},
"image": "openclaw-sandbox-common:bookworm-slim",
"network": "bridge",
"user": "1000:1000",
"setupCommand": [
"cat >/workspace/.profile <<'EOF'",
"if [ -f /run/secrets/linear_api_key ]; then",
" export LINEAR_API_KEY=\"$(cat /run/secrets/linear_api_key)\"",
"fi",
"EOF",
"chmod 0600 /workspace/.profile"
]
},
"mode": "all",
"scope": "session",
"workspaceAccess": "ro"
}
So:
- keep the bind
- keep
user: "1000:1000" - remove the
/etc/profile.dbits - use
/workspace/.profile
Then run:
openclaw sandbox recreate --all
I’m pretty confident this is the right shape for your current sandbox config.
:34.452+00:00 [diagnostic] lane task error: lane=main durationMs=14074 error="Error: /bin/sh: 1: cannot create /workspace/.profile: Read-only file system
Apr 15 17:35:34 ip-172-26-7-36 node[364271]: chmod: cannot access '/workspace/.profile': No such file or directory"
Ah, I see it now. I misread the workspaceAccess: "ro" behavior earlier, sorry.
With your config, both of my write-based ideas fail:
/etc/profile.d/...fails because you’re non-root and the rootfs is read-only/workspace/.profilefails because withworkspaceAccess: "ro",/workspaceis mounted read-only too
So the clean fix is: don’t write inside the container at all.
Instead, bind-mount a small helper script from the host straight into /etc/profile.d/linear.sh.
Host files
/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key
lin_api_xxx
/home/ubuntu/.openclaw/workspace/.secrets/linear.sh
if [ -f /run/secrets/linear_api_key ]; then
export LINEAR_API_KEY="$(cat /run/secrets/linear_api_key)"
fi
New binds
"binds": [
"/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro",
"/home/ubuntu/.openclaw/workspace/.secrets/linear.sh:/etc/profile.d/linear.sh:ro"
]
Then remove setupCommand
You shouldn’t need it anymore.
So your Docker config becomes roughly:
"docker": {
"binds": [
"/home/ubuntu/.openclaw/workspace/.secrets/linear_api_key:/run/secrets/linear_api_key:ro",
"/home/ubuntu/.openclaw/workspace/.secrets/linear.sh:/etc/profile.d/linear.sh:ro"
],
"env": {},
"image": "openclaw-sandbox-common:bookworm-slim",
"network": "bridge",
"user": "1000:1000"
}
Then recreate the sandbox:
openclaw sandbox recreate --all
This should fit your current sandbox config much better, because it avoids all container-internal writes.
$ /etc/profile.d/linear.sh
/bin/sh: 2: /etc/profile.d/linear.sh: Permission denied
$
That specific error is probably just because you’re executing the file, not sourcing it.
/etc/profile.d/linear.sh is meant to be read by the shell during login, so it does not need to be executable.
Good test
/bin/sh -lc 'printf "%s\n" "$LINEAR_API_KEY"'
or manually source it:
. /etc/profile.d/linear.sh
printf "%s\n" "$LINEAR_API_KEY"