Docs/Using Flowly/Sandbox & exec approvals
Using Flowly

Sandbox & exec approvals

Flowly runs shell commands through a security pipeline — structural analysis, a policy gate, and per-command approvals — while an optional OS-level sandbox constrains the agent process. This page covers how exec is gated, how approvals work, the sandbox, and runtime working-directory tracking.

The exec security pipeline

Every command the agent wants to run goes through these stages, in order:

  1. Enabled check — if the exec tool is disabled, the command is denied.
  2. Analyze — the command is parsed. A structurally unusable command (empty, contains a null byte, unparseable) is hard-rejected here.
  3. Protected-path floor — if the command touches a protected path (SSH keys, AWS credentials, the macOS Keychain, /etc/shadow, and similar), it is hard-rejected regardless of mode. This is not whitelistable and no approval can grant access.
  4. Policy gate — the security mode, ask mode, and allowlist decide whether to allow, deny, or prompt (see below).
  5. Approval — if a prompt is required, a pending approval is created and the agent waits for your decision.
  6. Execute — the command runs with the configured timeout.

The policy is re-read from disk if its file changed, so a long-running gateway picks up CLI/TUI edits without a restart.

Policy: security mode × ask mode

The policy has two independent dimensions.

Security mode (security):

ValueBehavior
denyAll exec commands denied.
allowlistOnly commands whose resolved path matches the allowlist (or are safe bins) run; others miss and are denied or prompted.
fullEverything is permitted (subject to the ask mode and the protected-path floor).

Ask mode (ask):

ValueBehavior
offNever prompt (except risky-command force-approval, below).
on-missPrompt on an allowlist miss. Only has effect in allowlist security mode — in full mode on-miss does nothing.
alwaysPrompt on every command.

Defaults: security="full", ask="off". Out of the box, exec runs without prompting.

Risky-command force-approval

A command flagged as risky (dangerous patterns, subshells/substitution, redirects, multi-line) always prompts — unless the policy is exactly security=full + ask=off.

"Full trust with no asking" (

security=full + ask=off) literally means no prompts even for dangerous patterns. Tighten ask to on-miss or always, or use allowlist mode, if you want a safety net.

Allowlist and safe bins

In allowlist mode, a command is permitted if it matches the allowlist or is a safe bin:

  • Allowlist entries are glob patterns matched against the command's resolved absolute path (leading ~ is expanded). A command whose executable cannot be resolved can never be allowlisted nor remembered with "Allow always".

  • Safe bins are a fixed set of stdin-only, read-only tools:

    jq grep cut sort uniq head tail tr wc cat echo date whoami pwd hostname uname

    (On Windows: findstr where ver type tasklist systeminfo are added.) A safe bin only stays safe if none of its arguments looks like a path or names an existing file — so they cannot be used to read arbitrary files via args.

The approval prompt

When a command needs approval, you get a prompt on your active surface showing the command and why it prompted (the resolved path and risk reasons). Three decisions:

KeyLabelMeaning
aAllow onceRun this command now; do not remember it.
sAllow alwaysRun it now and add its resolved path to the allowlist.
dDenyReject the command.

Allow always is only offered when the command has a resolvable path (pipelines and shell builtins can't be remembered). Press Esc/q to close without deciding. There is no "for session" button.

If no decision arrives before the approval timeout, the command is denied.

Plan mode: approving the work, not the command

Everything above gates one command at a time, as the agent reaches it. Plan mode gates the whole task, up front: the agent proposes a plan, and until you approve it, every side-effecting tool — exec included, but also file writes, email, integrations, and subagents — is refused before it runs.

The two layers are independent and compose:

Exec approvalsPlan mode
GatesA single shell commandEvery side-effecting tool
AsksWhen the command is reachedBefore any work starts
You approveThe commandThe plan
Timeout meansDenied (that command)Not approved (nothing runs)
Configured bysecurity / ask policyShift+Tab to ▣ Plan, or /plan

Plan mode sets no exec policy of its own — your security/ask settings keep applying inside an approved plan. It's an extra gate in front of them, not a replacement. So an approved plan running under security=full + ask=off still executes its commands without per-command prompts; you approved the shape of the work, not each step of it.

Managing policy from the CLI

The flowly approvals command reads and writes the policy:

flowly approvals status                 # show security / ask / ask_fallback / allowlist count
flowly approvals set --security full --ask off
flowly approvals set -s allowlist -a on-miss
flowly approvals list                   # table of allowlist entries
flowly approvals add "/usr/bin/git"     # add a glob pattern to the allowlist
flowly approvals remove "/usr/bin/git"  # remove a pattern
flowly approvals safe-bins              # list the built-in safe bins

--security accepts deny, allowlist, or full; --ask accepts off, on-miss, or always. Invalid values exit non-zero.

Managing policy and approvals from the TUI

Command / keyOpens
/approvalsThe pending-request queue.
/approvals permissions or /approvals policyThe permissions / policy editor.
/permissions (or /policy)The permissions / policy editor.
F3The pending-request queue.

In the pending queue you decide approvals with the same Allow once / Allow always / Deny choices. The policy editor applies security/ask changes and allowlist removals live.

Where the policy lives

The exec policy and allowlist do

not live in config.json. They live in the approvals store at ~/.flowly/credentials/exec-approvals.json.

~/.flowly/credentials/exec-approvals.json

The file is written with mode 0600 and serialized by a file lock. Stored defaults: security="full", ask="off", ask_fallback="deny", empty allowlist. Legacy tools.exec.security / tools.exec.ask keys in config.json are ignored and migrated once.

Exec runtime knobs (not the policy) do live in config.json under tools.exec:

{
  "tools": {
    "exec": {
      "enabled": true,
      "timeoutSeconds": 300,
      "maxOutputChars": 200000,
      "approvalTimeoutSeconds": 120,
      "cronMode": "deny"
    }
  }
}

cronMode (deny or approve) decides what happens when an exec approval is requested from inside a cron run: approve auto-allows once, deny rejects.

The OS sandbox

When enabled, Flowly re-executes itself inside an OS sandbox so the whole agent process — including any command it runs — is constrained by kernel rules. The sandbox restricts the filesystem; it does not currently restrict network egress.

Toggle precedence

The sandbox decision is made in this order:

  1. FLOWLY_SANDBOX env opt-out — if set to 0, false, off, or no (case-insensitive), the sandbox is disabled.
  2. security.sandbox config — read from ~/.flowly/config.json, default-on (a missing field, missing config, or read error all mean on; only an explicit false disables).
  3. Platform — macOS uses sandbox-exec, Linux uses bwrap. Windows and other platforms run unsandboxed (roadmap).
{
  "security": {
    "sandbox": true
  }
}
FLOWLY_SANDBOX=0 flowly ...   # run this invocation without the sandbox

What it enforces

  • macOS (sandbox-exec): allow by default, but deny reads of sensitive home subpaths (.ssh, .aws, .config/gcloud, Library/Keychains, browser profiles/cookies, .mozilla, …) and deny writes except under ~/.flowly, $HOME, and temp dirs. Subprocesses inherit the profile.

    One visible consequence: because Library/Keychains is hidden, Flowly's own credentials can't use the OS keychain while sandboxed either. It doesn't try — it writes them to 0600 files under ~/.flowly/credentials/ and says so. (Trying would make macOS raise a "keychain cannot be found" panel mid-setup, caused by this policy rather than by anything wrong with your Mac.)

  • Linux (bwrap): root mounted read-only, a fresh /proc, a tmpfs /tmp, read-write only for ~/.flowly, $HOME, and /tmp, sensitive paths masked, PID/UTS/IPC namespaces unshared, dies with parent.

Warning

Network is currently wide open on both platforms (macOS (allow network*), Linux --share-net). The sandbox is a filesystem boundary, not an egress filter — your firewall is responsible for egress.

The sandbox

fails open: if sandbox-exec/bwrap is missing or the wrapper can't start, Flowly runs unsandboxed rather than crashing, and does not surface a warning in this path.

Runtime working directory

Exec (and codex sessions) run in a runtime cwd — your project directory — which is separate from Flowly's internal workspace_path (~/.flowly/workspace). The runtime cwd is resolved per call, first match wins:

  1. An explicit working_dir on the tool call (honored verbatim, only ~-expanded).
  2. The per-session cwd (see cd persistence below).
  3. The FLOWLY_CWD environment variable.
  4. The agents.defaults.cwd config value.
  5. Workspace fallback → config.workspace_path → home.
{
  "agents": {
    "defaults": {
      "cwd": "~/projects/myapp"
    }
  }
}
FLOWLY_CWD=~/projects/myapp flowly ...

cd persistence per session

Within a session (non-Windows), a cd in one exec call persists to the next. The command is wrapped so its real exit code and stdout/stderr are unchanged; the final working directory is captured to a temp file and saved as the session's cwd. Per-session cwd lives in an in-process, lock-guarded map — there is no global os.chdir and no env mutation, so concurrent sessions don't clobber each other. A directory that no longer exists is ignored and re-validated next call. Non-session calls (cron, one-offs) are never wrapped.

Limitations

Warning

The process and docker tools bypass the exec approval/safety pipeline. Long-running commands launched through the process tool and Docker operations through the docker tool run with no command analysis, no approval flow, and no environment scrubbing — background processes inherit the full environment, and destructive Docker operations (prune, rm -f, compose down) are not gated by the exec allowlist. Treat these tools as more privileged than exec.

  • security=full + ask=off runs risky commands without prompting (the force-approval carve-out). Set ask to on-miss or always, or use allowlist mode, if you want prompts.
  • The sandbox does not filter network egress in the current phase — it is a filesystem boundary only.
  • The sandbox fails open silently if its wrapper is unavailable.