Safety and Sandboxing
Swival's built-in sandbox is implemented at the application layer. It validates paths and enforces command policy in Python, but it is not an operating-system isolation boundary. You should treat it as a strong guardrail for normal use, not as a hard security perimeter against untrusted or adversarial models.
If you need stronger isolation, Swival integrates with two OS-level sandbox runtimes: AgentFS (copy-on-write filesystem overlay) and nono (Landlock/Seatbelt capability enforcement with optional network filtering and rollback).
AgentFS Sandbox Mode
Pass --sandbox agentfs to run Swival inside an AgentFS overlay. At startup, Swival re-executes itself inside agentfs run, which provides copy-on-write filesystem isolation. The agent can edit files and run commands freely, but writes are confined to the overlay — your real project tree stays untouched until you copy changes back.
swival --sandbox agentfs "Refactor the auth module" --yolo
In this mode, --base-dir and each --add-dir path are mapped to AgentFS --allow rules so the agent can write to those directories inside the overlay. Everything else on the host filesystem is read-only to subprocesses.
Swival automatically generates a deterministic session ID from the project directory, so re-running swival --sandbox agentfs in the same directory reuses the overlay. You can see the session ID and a resume command when diagnostics are enabled (the default unless --quiet is set). To provide your own session ID instead:
swival --sandbox agentfs --sandbox-session my-feature "Continue the refactor" --yolo
To get a fresh, ephemeral overlay with no session reuse:
swival --sandbox agentfs --no-sandbox-auto-session "One-off task" --yolo
After a run, Swival prints a diff hint showing how to review changes (unless --quiet is set):
Review changes: agentfs diff swival-a1b2c3d4e5f6
This requires the agentfs binary on PATH. If it is not found, Swival exits with an actionable error. See Using Swival With AgentFS for more workflows.
Strict Read Mode
By default, the AgentFS sandbox only isolates writes — the agent can still read any file on the host filesystem. Pass --sandbox-strict-read to also restrict reads to explicitly allowed directories:
swival --sandbox agentfs --sandbox-strict-read "Analyze the project" --yolo
This flag requires an AgentFS version that supports strict read isolation. No current release supports it yet, so using the flag today produces a clear error with the installed version. When AgentFS ships the feature, Swival will detect it automatically and pass the appropriate flags through.
You can also combine sandbox-exec with AgentFS when you want additional kernel-level controls like network restriction:
sandbox-exec -p '(version 1)(allow default)(deny network*)' \
swival --sandbox agentfs "task" --yolo
nono Sandbox Mode
Pass --sandbox nono to run Swival inside a nono sandbox. Like the AgentFS path, Swival re-executes itself early in startup — this time as nono run -- swival .... nono enforces filesystem and network boundaries at the kernel level, using Landlock on Linux and Seatbelt on macOS. The supervising parent process stays outside the sandbox and provides the audit trail, network proxy, and rollback machinery; the child Swival runs with the enforced capability set.
swival --sandbox nono "Refactor the auth module" --yolo
The base directory is granted read+write (so the agent can edit your project), and each --add-dir path becomes an additional grant. nono's default system-path allowances cover the Python interpreter and standard libraries. If Swival is installed under an unusual prefix that nono does not allow by default, add that directory with --add-dir so the interpreter stays reachable.
Unlike AgentFS, nono enforces by path rather than overlaying the working directory, so writes land on your real files. Enable rollback snapshots when you want a safety net:
swival --sandbox nono --nono-rollback "Try a risky migration" --yolo
After the run, Swival prints a hint for reviewing or undoing the changes (unless --quiet is set):
Review changes: nono rollback
nono can also filter the agent's network access. Block it entirely, or restrict it to an allowlist:
swival --sandbox nono --nono-block-net "Offline analysis" --yolo
swival --sandbox nono --nono-allow-domain api.openai.com --nono-allow-domain github.com "task" --yolo
The full set of nono knobs:
| Flag | Effect |
|---|---|
--nono-profile <name> |
Apply a named nono profile |
--nono-rollback |
Take atomic rollback snapshots for the session |
--nono-block-net |
Deny all outbound network |
--nono-allow-domain <host> |
Add a domain to the proxy allowlist (repeatable) |
--nono-network-profile <name> |
Apply a preset domain group |
--nono-credential <service> |
Inject credentials for a service via nono's proxy (repeatable) |
--nono-audit-integrity |
Add filesystem-state hashing to the audit log |
All of these are accepted only with --sandbox nono; using one without it is an error. They can also be set in the config file (nono_profile, nono_rollback, nono_block_net, nono_allow_domain, nono_network_profile, nono_credential, nono_audit_integrity).
Some providers need access to credentials stored on disk. Using --provider chatgpt under nono grants the sandbox read/write access to LiteLLM's local OAuth state directory (~/.config/litellm) so the provider can authenticate. For stronger credential isolation, prefer nono credential proxy support via --nono-credential once it is available for your provider flow.
This requires the nono binary on PATH. If it is not found, Swival exits with an actionable error. See Using Swival With nono for practical workflows.
--sandbox nono is a CLI feature: the automatic re-exec only happens for the swival command. Library callers using the Session API are not re-executed — instead, launch your own process under nono and pass sandbox="nono":
nono run --allow . -- python my_agent.py
Inside that wrapped process, Session(sandbox="nono") detects the sandbox and proceeds; outside it, the session fails fast with a clear error rather than running unsandboxed.
Base Directory Enforcement
All filesystem operations are anchored to --base-dir, which defaults to the current directory. Path checks resolve both the base directory and target path through symlinks, then verify that the resolved target remains inside an allowed root. If a path escapes through traversal or symlink indirection, the operation fails.
Even with --files all, Swival blocks the filesystem root itself. You cannot grant the agent access to / by accident.
Additional Allowed Directories
When the agent needs full access outside --base-dir, pass one or more --add-dir flags.
swival --add-dir ~/shared-data --add-dir /opt/configs "Update the config"
When the agent only needs to read files without modifying them, use --add-dir-ro instead.
swival --add-dir-ro ~/reference-docs --add-dir-ro /opt/datasets "Analyze the data"
Both flags can be combined. The agent gets read-write access to --add-dir paths and read-only access to --add-dir-ro paths.
swival --add-dir ./output --add-dir-ro ~/corpus "Summarize the corpus into output/"
Each allowed directory must already exist, must be a directory, and cannot be the filesystem root. In REPL mode, you can grant the same access dynamically with /add-dir <path> or /add-dir-ro <path>.
Command Execution Policy
Command execution is unrestricted by default (--commands all). You can restrict or disable it.
In whitelist mode, you pass a comma-separated set of command basenames. Pass "none" to disable commands entirely. Pass "ask" to require interactive approval for every command bucket.
swival --commands ls,git,python3 "task"
swival --commands ask "task"
swival --commands none "task"
At startup, each basename is resolved to an absolute path using which. If a command cannot be found, Swival exits with an error. If a command resolves inside your base directory, Swival rejects it so the agent cannot modify and execute workspace binaries in one session.
At runtime in whitelist mode, only run_command is available and commands must be passed as argument arrays. This removes shell interpolation and injection risk from ordinary command calls. run_shell_command is not exposed in whitelist mode since shell strings bypass the whitelist entirely.
With --commands all or --yolo, both run_command and run_shell_command are available. In ask mode, only run_command is exposed — shell access requires --commands all.
Ask Mode
In ask mode (--commands ask), Swival prompts you before running each new command category. Commands are grouped into buckets by their base name (e.g. ls, git push, python3 -m pytest).
Only run_command is available, so commands must be passed as argument arrays, which prevents shell injection. Once you approve a bucket, subsequent commands in the same bucket run without asking again.
High-risk buckets (rm, git push, docker, curl, interpreter inline-code like bash -c or python3 -c) default to deny — you must explicitly type y to allow them. Non-high-risk buckets default to allow on Enter.
Approval options:
Enter— allow (non-high-risk) or deny (high-risk)y— allow this bucket for the rest of the sessionn— deny this bucket for the rest of the sessionp— allow and persist the approval to.swival/approved_bucketso— allow this one invocation onlya— always re-prompt for this bucket
Subagents cannot prompt interactively. In ask mode, subagents can only run commands in buckets that are already approved — either pre-approved via approved_buckets in config, or runtime-persisted in .swival/approved_buckets from earlier interactive sessions.
# swival.toml — intentional pre-approvals (version-controllable)
commands = "ask"
approved_buckets = ["ls", "git status", "python3 -m pytest"]
One-Shot Command Dispatch
In one-shot mode, / and ! command dispatch is disabled by default. When input comes from a pipe, a file, or an upstream program, it may contain attacker-controlled content that starts with a slash or bang command. Executing that input as a command could run scripts from ~/.config/swival/commands/ or alter the agent's security posture (e.g. /add-dir /).
Pass --oneshot-commands to opt in when you trust the input source:
swival --oneshot-commands "/simplify swival/agent.py"
Or set it in config:
oneshot_commands = true
--yolo does not imply --oneshot-commands. They control different trust boundaries: --yolo governs what the agent can do (filesystem and shell access), while --oneshot-commands governs whether the input itself is trusted to contain commands.
In interactive mode, the user is typing directly, so commands are always enabled regardless of this flag.
Untrusted Content Labels
Output from external sources — fetch_url, MCP tools, and A2A tools — is wrapped with a deterministic [UNTRUSTED EXTERNAL CONTENT] header before the model sees it. This header instructs the model to treat the content as data, not instructions, and to avoid changing tool-selection behavior based on it.
The label is baked into spill files too. When an external tool produces output too large for inline context (over 20 KB for MCP/A2A, over 50 KB for fetch_url), the content is saved to a temp file under .swival/. The untrusted header is prepended to the file contents, so the label survives when the agent reads the file back via read_file.
Filesystem Access Policy
--files controls what the filesystem tools can access. It accepts "some" (the default), "all", or "none".
In the default mode (--files some), filesystem tools are restricted to the base directory and any --add-dir / --add-dir-ro paths. In --files all mode, the agent can read or write any non-root path. In --files none mode, only the .swival/ directory is accessible through tools — the agent can still think, run commands, and fetch URLs.
swival --files all "do whatever you want"
swival --files none --commands ls,git "read-only analysis"
--yolo is shorthand for --files all --commands all. If you also pass an explicit --files or --commands, the explicit flag wins.
swival --yolo "unrestricted access"
swival --yolo --files none "commands only, no file access"
Read-Before-Write Guard
By default, Swival blocks writes to existing files unless that file has already been read or previously written during the current session. This reduces accidental overwrites when the model has not inspected current file contents.
This guard also applies when write_file uses move_from and the destination already exists. The source path is exempt from the read requirement because renaming does not modify source content.
If you intentionally want direct write access without prior reads, disable the guard with --no-read-guard.
swival --no-read-guard "task"
URL Fetching And SSRF Protections
The fetch_url tool only allows http and https. It resolves each hostname with socket.getaddrinfo, blocks private and internal address classes through ipaddress, and re-runs those checks on every redirect hop.
Loopback addresses are allowed when the hostname is explicitly localhost, 127.0.0.1, or ::1, so agents can test locally running servers. Arbitrary hostnames that resolve to loopback addresses are still blocked to prevent DNS rebinding attacks. Redirect chains are handled manually and capped at ten hops.
Binary MIME types are rejected. Response bodies are capped at 5 MB before conversion, and converted inline output is capped at 50 KB.
Output Caps
Several hard caps keep the conversation bounded. File reads are limited to 50 KB per call and lines are truncated at 2,000 characters. Directory and grep-style listings are capped at 100 results. The 50 KB cap (which also bounds listings, grep, outline, and URL fetches) and the default 2000-line read limit can be changed with the max_output_kb and max_output_lines settings.
Command output is capped at 10 KB inline, with larger output written to .swival/ (hard-capped at 1 MB) for paginated reads and auto-cleaned after roughly ten minutes.
MCP tool output uses higher thresholds: 20 KB inline, with larger output written to .swival/ and hard-capped at 10 MB before writing. MCP error output is inline-capped at 20 KB without file save.
URL fetch output is capped at 50 KB inline, with larger output saved to files.
Response history is written to .swival/HISTORY.md, which is capped at 500 KB. When a new entry would exceed the cap, the oldest entries are trimmed to make room.