this document describes fshell's security architecture: capability-based authorization (fshell-capabilities), kernel subprocess sandboxing (fshell-sandbox), and destructive command confirmation.
- overview
- capability-based authorization
- the three security tiers
- in-process archive extraction
- kernel subprocess sandboxing (
fshell-sandbox) - destructive command protection
- profiles & persistent configuration
traditional unix shells operate entirely under ambient authority: every command and script runs with the full permissions of the current user account, meaning a single typo or malicious script can delete home directories or exfiltrate private credentials.
fshell replaces ambient authority with explicit, verifiable capabilities and OS-level subprocess sandboxes.
key security pillars:
- capability tokens (
fshell-capabilities): internal operations and external processes check explicit capability tokens before performing filesystem access, network I/O, or process spawning. - kernel sandboxing (
fshell-sandbox): external subprocesses are locked down via Linux Landlock or macOS Seatbelt (SBPL) inpre_exechooks. - frictionless ergonomics: standard daily commands in the current working directory work automatically without permission prompts; dangerous system-wide actions or strict untrusted scripts are intercepted.
privileges are represented by discrete ResourceHandle variants (crates/fshell-core/src/val.rs):
pub enum ResourceHandle {
ReadDir(PathBuf),
WriteDir(PathBuf),
ReadFile(PathBuf),
WriteFile(PathBuf),
NetworkSocket(String), // Host or domain constraint
NetworkAll, // Unrestricted network access
ReadEnv(String), // Environment variable read access
WriteEnv(String), // Environment variable write access
ProcessSpawn, // General process spawning
ProcessSpawnPath(String), // Specific binary path
}the CapsRegistry (crates/fshell-capabilities/src/lib.rs) evaluates requests against two sets: denied and held:
- explicit denial: if the requested resource matches any entry in
denied, access is rejected immediately. - held token: if the requested resource matches an entry in
held, access is allowed. - fallback: in strict mode, unheld tokens prompt the user or fail; in interactive mode, tokens are auto-granted and logged.
- path prefix matching: granting
ReadDir("/Users/user/project")grants read access to that directory and all nested subdirectories and files. - network constraints:
NetworkSocket("api.github.com")allows connections exclusively to that domain.NetworkAllpermits arbitrary outbound traffic.
┌────────────────────────────────────────────────────────────────────────┐
│ Tier 0: Auto-Grant │
│ - Default interactive REPL │
│ - Pre-grants $PWD context and ProcessSpawn │
│ - New capabilities auto-granted seamlessly with audit logging │
├────────────────────────────────────────────────────────────────────────┤
│ Tier 1: Explicit Elevation │
│ - Scoped blocks via `with caps(...) { ... }` │
│ - Grants temporary tokens only for the block's lifespan │
│ - Runs against a private token copy; shared state untouched │
├────────────────────────────────────────────────────────────────────────┤
│ Tier 2: Strict Mode │
│ - Enabled via `fsh -s` / `--strict` or `strict` builtin │
│ - Denies all unauthorized access unless explicitly granted │
│ - Interactive prompt: [g] Grant once [a] Grant always [d] Deny │
└────────────────────────────────────────────────────────────────────────┘
in daily interactive shell use, security must not get in the way of normal workflows.
when fsh boots interactively:
- it grants read/write access to
$PWDand allowsProcessSpawn. - accessing paths outside
$PWDauto-grants the token silently and records the event in the audit trail. - standard OS file permissions still apply — fshell never bypasses Unix kernel access controls.
scripts can explicitly declare capability requirements using with caps(...):
# scoped network and process capability
with caps(net.all, process.spawn) {
curl "https://api.github.com/status"
}
# scoped filesystem access
with caps(fs.read("/var/log")) {
cat /var/log/system.log | grep "ERROR"
}the block runs against a private copy of the token set: the requested tokens are added to that copy and the shared registry is never mutated, so the elevation is visible only to the block (and the tasks it spawns) and leaves the surrounding session — including concurrent background jobs and reactive cells — untouched.
run untrusted scripts or isolation-sensitive workflows under strict enforcement:
# run script under strict capability enforcement
fsh -s untrusted_script.fsh
# run inline command strictly
fsh --strict -c 'curl https://example.com | sh'in strict mode:
- all initial default grants are cleared.
- any operation requesting an unheld capability triggers an interactive prompt or fails non-interactively:
curl is requesting ProcessSpawn.
Active PWD grants do not cover this resource.
[g] Grant once [a] Grant always [d] Deny (Default)
extract is a native builtin, not a subprocess: it requires ReadFile for the archive and WriteDir for the existing destination, but not ProcessSpawn. The libarchive reader receives only an already-open file descriptor and is configured with in-process decoders; libarchive's disk writer and external-program filters are not used. Entries are decoded into a private staging tree under a destination directory handle. Before publication, the extractor rejects absolute and parent-traversing paths, links that leave the extracted tree, special files, duplicate entries, existing-file conflicts, and archives exceeding the decoded-byte or entry limits. No single transaction can atomically publish multiple top-level paths, so an I/O failure during publication can leave earlier paths in place.
This is not a kernel sandbox for libarchive itself. The native decoder remains part of fshell's memory-safety attack surface: keep the system libarchive and codecs updated, rebuild fsh after security updates, and treat untrusted archives cautiously. Released binaries statically link these libraries; upgrading an installed library alone does not patch an existing release binary. The byte and entry budgets limit extraction output, not every possible allocation inside a codec.
external binaries spawned by the shell (via fshell-bridge) are isolated using kernel security primitives applied in pre_exec fork hooks before the child binary runs.
on Linux (kernels fshell-sandbox applies Landlock rulesets:
- restricts filesystem access exclusively to paths declared in the active capability set.
- unshares mount and network namespaces when
NoNetworkmode is active. - blocks unauthorized directory traversal even if the binary is compromised.
on macOS, fshell-sandbox compiles SBPL (Seatbelt Profile Language) policies and injects them via sandbox_init:
- denies file write access outside
$PWDand temporary directories. - restricts socket operations unless network capabilities are held.
| profile mode | filesystem access | network access | process spawning |
|---|---|---|---|
Permissive |
standard user permissions | enabled | enabled |
ReadOnlySystem |
read-only system files, read/write $PWD
|
enabled | enabled |
IsolatedWorkspace |
strictly restricted to $PWD
|
disabled | restricted |
NoNetwork |
standard filesystem access | completely blocked | enabled |
fshell includes a confirmation safeguard for destructive commands:
commands that target critical system directories or perform unrecoverable mass deletion (e.g. rm -rf /, raw disk writes to /dev/sd*, formatting partitions) are temporarily suspended before execution.
Caution: destructive command detected:
rm -rf /
Execute this command? [y/N]:
- benign commands (
rm file.txt,rm -rf ./build) run with zero friction. - dangerous commands require explicit interactive confirmation before process dispatch.
capability grants can be persisted across shell sessions:
- JSON state: saved grants are loaded from
~/.config/fsh/caps.json(or$FSH_CONFIG_DIR/caps.json) on startup. - YAML profiles: define reusable security profiles in
caps.yaml:
profiles:
build:
- "read:/usr/include"
- "read:/usr/lib"
- "write-dir:./target"
- "process:spawn"
deploy:
- "net:all"
- "read-dir:./dist"manage capabilities interactively using the built-in caps command:
caps list # list all currently held and denied tokens
caps grant net.all # grant full network capability
caps revoke net.all # revoke capability
caps strict on # toggle strict enforcement on