Skip to content

Security: FraSharp/fshell

Security

docs/SECURITY.md

fshell security model

this document describes fshell's security architecture: capability-based authorization (fshell-capabilities), kernel subprocess sandboxing (fshell-sandbox), and destructive command confirmation.


table of contents


overview

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:

  1. capability tokens (fshell-capabilities): internal operations and external processes check explicit capability tokens before performing filesystem access, network I/O, or process spawning.
  2. kernel sandboxing (fshell-sandbox): external subprocesses are locked down via Linux Landlock or macOS Seatbelt (SBPL) in pre_exec hooks.
  3. 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.

capability-based authorization

ResourceHandle variants

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
}

deny-then-allow evaluation

the CapsRegistry (crates/fshell-capabilities/src/lib.rs) evaluates requests against two sets: denied and held:

$$\text{Request} \longrightarrow \text{Denied Check} \xrightarrow{\text{not denied}} \text{Held Check} \xrightarrow{\text{held}} \text{Allowed}$$

  1. explicit denial: if the requested resource matches any entry in denied, access is rejected immediately.
  2. held token: if the requested resource matches an entry in held, access is allowed.
  3. fallback: in strict mode, unheld tokens prompt the user or fail; in interactive mode, tokens are auto-granted and logged.

path & network scoping

  • 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. NetworkAll permits arbitrary outbound traffic.

the three security tiers

┌────────────────────────────────────────────────────────────────────────┐
│                        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      │
└────────────────────────────────────────────────────────────────────────┘

tier 0: interactive auto-grant (default)

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 $PWD and allows ProcessSpawn.
  • accessing paths outside $PWD auto-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.

tier 1: scoped explicit elevation (with caps)

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.

tier 2: strict mode (--strict)

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)

in-process archive extraction

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.

kernel subprocess sandboxing (fshell-sandbox)

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.

linux: landlock security

on Linux (kernels $\ge 5.13$), fshell-sandbox applies Landlock rulesets:

  • restricts filesystem access exclusively to paths declared in the active capability set.
  • unshares mount and network namespaces when NoNetwork mode is active.
  • blocks unauthorized directory traversal even if the binary is compromised.

macos: seatbelt / sbpl

on macOS, fshell-sandbox compiles SBPL (Seatbelt Profile Language) policies and injects them via sandbox_init:

  • denies file write access outside $PWD and temporary directories.
  • restricts socket operations unless network capabilities are held.

sandbox profiles

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

destructive command protection

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.

profiles & persistent configuration

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

There aren't any published security advisories