PRAISON_SHELL Modes
Choose how praisonai code runs agent shell commands — refuse operators, contain every command, or run uncontained
PRAISON_SHELL picks the containment posture for every execute_command call in praisonai code: refuse shell operators, run every command inside an OS sandbox, or run operators in an uncontained shell.
graph TB
Cmd["🖥️ execute_command"] --> Mode{{"PRAISON_SHELL"}}
Mode -->|unset / off (default)| Off["Plain command → plain executor<br/>Operator → refused with hint"]
Mode -->|sandboxed| Jail{"🛡️ Probe enforcement"}
Jail -->|enforcing| Contained["Every command runs as<br/>/bin/sh -c inside Seatbelt / bwrap<br/>workspace writable, secrets scrubbed"]
Jail -->|not enforcing| Refuse["🚫 Refused — never degrade"]
Mode -->|unsafe (opt-in only)| Unsafe["Plain command → plain executor<br/>Operator → real /bin/sh, NO containment"]
classDef input fill:#189AB4,stroke:#7C90A0,color:#fff
classDef decision fill:#6366F1,stroke:#7C90A0,color:#fff
classDef safe fill:#10B981,stroke:#7C90A0,color:#fff
classDef warn fill:#F59E0B,stroke:#7C90A0,color:#fff
classDef danger fill:#8B0000,stroke:#7C90A0,color:#fff
class Cmd input
class Mode,Jail decision
class Contained,Off safe
class Refuse warn
class Unsafe dangerQuick Start
- Default — operators are refused
Without
PRAISON_SHELL, the coding agent runs plain commands as before, but a command with a shell operator fails loudly instead of silently dropping it.bash praisonai codetext >>> run: echo written > out.txt Refused: this command contains the shell operator '>', which this executor does not interpret. Do one of these instead: - split it into separate execute_command calls; - for a redirect, run the program and write its output with write_file; - or ask the operator to set PRAISON_SHELL=sandboxed. - Contain every command
Set
PRAISON_SHELL=sandboxedto run every command — with or without operators — as a real/bin/sh -cinside OS-native containment (Seatbelt on macOS,bwrapon Linux).bash PRAISON_SHELL=sandboxed praisonai codetext >>> run: echo written > out.txt [approve: execute_command — critical risk?] y exit=0 success=True sandbox=seatbelt
The Three Modes
PRAISON_SHELL selects one behaviour. It is an operator decision — the model cannot change it from a tool call.
PRAISON_SHELL | Behaviour |
|---|---|
unset / off (default) | Shell syntax is refused with an error naming the operator and listing alternatives. Plain commands run exactly as before. |
sandboxed | A real /bin/sh -c inside OS-native containment runs every command (with or without shell operators). The writable set is PRAISONAI_WORKSPACE (or the process cwd) plus $TMPDIR; a cwd outside it is refused. Secret-looking env vars (KEY, TOKEN, SECRET, PASSW, CREDENTIAL, case-insensitive) are stripped from the child; ~/.ssh, ~/.aws, ~/.gnupg, ~/.config/gcloud, ~/.azure are masked. Refuses to run if containment can’t be proven — see OS Sandbox. |
unsafe | Operator commands run in a real /bin/sh -c with no containment; plain commands run as in off. Opt-in only, never a fallback. |
Aliases resolve to these three: 1/true/yes/sandbox/native → sandboxed; raw/host → unsafe; empty/0/false/no/none → off.
What sandboxed protects against
Containment is a property of the mode, not of the command’s syntax. rm -rf build, python x.py, pip install pkg or curl -o x.sh URL have no operator, yet all run inside the jail.
import os
from praisonaiagents import Agent
from praisonai_code.cli.features.shell_exec import execute_command
# Operator decision: contained execution for every shell command
os.environ["PRAISON_SHELL"] = "sandboxed"
os.environ["PRAISONAI_WORKSPACE"] = "/path/to/workspace"
agent = Agent(
name="Builder",
instructions="Build the project and run the tests.",
tools=[execute_command],
)
agent.start("Build and test the current workspace")Workspace, env, and secret masking
| What | Under sandboxed | |||||
|---|---|---|---|---|---|---|
| Writable root | PRAISONAI_WORKSPACE, else the process cwd — plus $TMPDIR. Nothing else is writable. | |||||
cwd= argument | Must be inside the writable set; cwd="/" or your home directory is refused instead of becoming writable. | |||||
| Environment | Any variable whose name matches `KEY\ | TOKEN\ | SECRET\ | PASSW\ | CREDENTIAL (case-insensitive) is stripped, so env \ | grep KEY` shows nothing. |
Explicit env= | Acts as the allow-list. Entries are applied inside the jail via /usr/bin/env k=v …, never on the launcher, so LD_PRELOAD cannot load code before containment starts. Names must match ^[A-Za-z_][A-Za-z0-9_]*$. | |||||
| Secret directories | ~/.ssh, ~/.aws, ~/.gnupg, ~/.config/gcloud, ~/.azure are masked — cat ~/.aws/credentials returns nothing. The rest of the filesystem stays readable for toolchains. | |||||
| Processes (Linux) | bwrap --unshare-pid gives the jail its own PID namespace, so /proc/<host pid>/environ cannot leak the scrubbed env or masked paths. |
Guarantees against model misuse
The tool-facing execute_command drops mode, writable_paths and network from its arguments. A tool call cannot pick mode="unsafe", writable_paths=["/"] or network=True — the mode and the jail are set by the operator through env vars.
Cancellation. A contained command stops when the agent is interrupted: the process tree is killed, output is capped, and the result reports exit_code=130 with error="Command interrupted by user".
ACP in sandboxed mode. Headless praisonai code -p exposes acp_execute_command. Under sandboxed, its approved plan steps run inside the same jail with the workspace as the writable root, without a second approval prompt. Under off and unsafe, the ACP path still refuses ;, &, |, $ and backticks.
Why the default refuses
The plain executor runs shlex.split + subprocess.Popen(shell=False), so an operator becomes a literal argument.
execute_command("echo written > redir.txt")
# → exit_code=0, success=True, stdout='written > redir.txt\n'
# and redir.txt was never createdThe model is told a redirect it never performed succeeded — strictly worse than an error. Refusing names the operator and tells the agent what to do instead.
Quote-Aware Detection
Detection looks for operators outside quotes, so ordinary strings are never refused.
| Command | Refused? | Why |
|---|---|---|
echo "a > b" | No | > is inside double quotes |
git commit -m "fix: a > b" | No | > is inside the message |
echo 'a $(id) b' | No | single quotes suppress everything |
echo "$(id)" | Yes ($() | command substitution runs inside double quotes |
echo "id" | Yes (backtick) | backticks run inside double quotes |
a && b | Yes (&&) | active operator |
$(...) and backticks are detected even inside double quotes because POSIX shells evaluate them there — reporting them as inert would reintroduce the false-success bug.
Approval
The real-shell path used by execute_command is wrapped in require_approval(risk_level="critical") under the tool name execute_command.
ACP plan steps under sandboxed reuse the plan-level approval and do not re-prompt per command.
The approval wrap fails closed: if the approval machinery can’t be imported, the real shell does not run.
When Your Agent Hits a Refusal
An agent that hits PRAISON_SHELL=off refusal has three honest paths.
from praisonaiagents import Agent
agent = Agent(
name="Builder",
instructions=(
"When execute_command refuses a shell operator, either split the "
"command into separate steps, or write program output with write_file "
"instead of a '>' redirect."
),
)
agent.start("Build the project and save the log to build.log")The operator can instead launch the session with PRAISON_SHELL=sandboxed to allow real operators inside a jail.
Under sandboxed, a command can be refused even without an operator:
| Refusal | Fix |
|---|---|
cwd '…' is outside the sandbox workspace | Run from inside PRAISONAI_WORKSPACE, or point PRAISONAI_WORKSPACE at the right project. |
run_in_background is not supported | Run the command in the foreground. |
containment could not be proven on this machine | Install the OS primitive (bwrap on Linux); set PRAISON_SHELL=unsafe only to accept an uncontained shell deliberately. |
Best Practices
Keep the default off in shared or untrusted contexts
off never runs an uninterpreted operator and never claims false success. Reach for sandboxed only when a real shell is genuinely needed.
Prefer sandboxed over unsafe
sandboxed gives a real /bin/sh while a kernel keeps writes inside the workspace. unsafe removes that boundary entirely — use it only when you accept an uncontained shell deliberately.
Split multi-step commands
Two execute_command calls (one per && step) work in every mode and keep each step’s success signal honest.
Write output instead of redirecting
Replace cmd > file.txt with a plain cmd call plus write_file. This works in off mode and keeps the file-creation step explicit.