The exec security pipeline
Every command the agent wants to run goes through these stages, in order:
- Enabled check β if the exec tool is disabled, the command is denied.
- Analyze β the command is parsed. A structurally unusable command (empty, contains a null byte, unparseable) is hard-rejected here.
- 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. - Policy gate β the security mode, ask mode, and allowlist decide whether to allow, deny, or prompt (see below).
- Approval β if a prompt is required, a pending approval is created and the agent waits for your decision.
- 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):
| Value | Behavior |
|---|---|
deny | All exec commands denied. |
allowlist | Only commands whose resolved path matches the allowlist (or are safe bins) run; others miss and are denied or prompted. |
full | Everything is permitted (subject to the ask mode and the protected-path floor). |
Ask mode (ask):
| Value | Behavior |
|---|---|
off | Never prompt (except risky-command force-approval, below). |
on-miss | Prompt on an allowlist miss. Only has effect in allowlist security mode β in full mode on-miss does nothing. |
always | Prompt 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.
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 systeminfoare 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:
| Key | Label | Meaning |
|---|---|---|
a | Allow once | Run this command now; do not remember it. |
s | Allow always | Run it now and add its resolved path to the allowlist. |
d | Deny | Reject 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.
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 / key | Opens |
|---|---|
/approvals | The pending-request queue. |
/approvals permissions or /approvals policy | The permissions / policy editor. |
/permissions (or /policy) | The permissions / policy editor. |
| F3 | The 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
not live in config.json. They live in the approvals store at ~/.flowly/credentials/exec-approvals.json.
~/.flowly/credentials/exec-approvals.jsonThe 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:
FLOWLY_SANDBOXenv opt-out β if set to0,false,off, orno(case-insensitive), the sandbox is disabled.security.sandboxconfig β read from~/.flowly/config.json, default-on (a missing field, missing config, or read error all mean on; only an explicitfalsedisables).- Platform β macOS uses
sandbox-exec, Linux usesbwrap. Windows and other platforms run unsandboxed (roadmap).
{
"security": {
"sandbox": true
}
}FLOWLY_SANDBOX=0 flowly ... # run this invocation without the sandboxWhat 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. - 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.
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.
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:
- An explicit
working_diron the tool call (honored verbatim, only~-expanded). - The per-session cwd (see
cdpersistence below). - The
FLOWLY_CWDenvironment variable. - The
agents.defaults.cwdconfig value. - 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
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=offruns risky commands without prompting (the force-approval carve-out). Setasktoon-missoralways, or useallowlistmode, 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.