Home
Why Sandlock
How It Works Use Cases Comparison Security Model
Docs
Documentation Home Getting Started CLI Reference Python SDK Sandbox Reference
Products
Overview Sandbox HTTP API Sandbox Scheduler
GitHub Schedule a Demo
Sandlock / Docs /Profiles and learn

Profiles and sandlock learn

A policy that has stopped changing belongs in a file rather than a command line. And you do not have to write that file by hand: sandlock can watch a real run and produce it for you.

Profiles

A profile is a TOML file in ~/.config/sandlock/profiles/, referenced by name with -p. The schema is sectioned, and flat top-level keys such as fs_readable = [...] are rejected rather than ignored, so a profile written against the wrong shape fails loudly instead of silently granting nothing.

~/.config/sandlock/profiles/build.toml
[program]
exec      = "make"
args      = ["-j4"]
clean_env = true
env       = { CC = "gcc", LANG = "C.UTF-8" }

[filesystem]
read  = ["/usr", "/lib", "/lib64", "/bin", "/etc"]
write = ["/tmp/work"]

[limits]
memory    = "512M"
processes = 50

[syscalls]
extra_deny = []
Using profiles
$ sandlock profile list
$ sandlock profile show build
$ sandlock profile delete build

# Runs [program].exec with [program].args
$ sandlock run -p build

# A trailing command overrides [program]
$ sandlock run -p build -- make test

# A profile that is not installed in the profiles directory
$ sandlock run --profile-file ./ci.toml -- make

Every policy section is available in a profile. The full schema, with each field's type and default, is in the sandbox reference; the sections are [config], [determinism], [program], [filesystem], [network], [http], [syscalls], and [limits].

Naming a sandbox instance. A profile describes the policy, not the instance. Pass --name when you need a stable virtual hostname for a particular run, for example when several sandboxes launched from the same profile need to be told apart by sandlock ps or reached by a reverse proxy.

sandlock learn

Writing a filesystem policy by hand for a real workload is tedious and error-prone: you find the missing paths one failed run at a time. sandlock learn runs the workload under observation and emits a profile covering the paths, network connections, and resource peaks it actually used. The result feeds straight into sandlock run -p.

Synopsis
sandlock learn [options] -- <cmd> [args...]
Flag Default Description
-o <file>stdoutWrite the profile to a file
--timeout <secs>noneKill the workload after N seconds and emit a partial profile
--collapse [N]offCollapse directories where N or more files were observed (default 4)
--collapse-prefix <path>noneForce collapse of everything under a prefix; repeatable
--force-sensitive-collapseoffAllow --collapse-prefix to target sensitive paths

What is recorded

Domain Mechanism
Filesystem readsseccomp-notify on openat and open
Filesystem writesThe same, classified by open flags (O_WRONLY, O_RDWR, O_CREAT)
Binaries and libraries/proc/<pid>/exe plus r-xp mappings from /proc/<pid>/maps
Network connectionsseccomp-notify on connect, sendto, sendmsg
Resource peaksSampling /proc/<pid>/status: RSS, thread count, fd count

Path collapsing

By default every observed path is recorded individually, which is precise and unreadable for a tree like /usr/lib. --collapse N aggregates directories where at least N files were touched.

After collapsing, a deduplication pass removes any individual path already covered by an ancestor in the list. Landlock grants are recursive, so the ancestor entry is sufficient and the descendant is noise.

Write collapse is automatic

Files created during the run do not exist on the real filesystem, because COW intercepted them. Landlock requires an existing path, so the collapser walks up to the nearest existing ancestor for writes. This is not optional: omitting the ancestor would produce a profile that causes sandlock run to abort.

Path tiers

Automatic collapsing on a sensitive directory is how a generated profile quietly becomes a bad one. learn classifies paths into three tiers and treats them differently.

Tier Paths Write (auto) --collapse N --collapse-prefix
Protected /, /root, ~/.ssh, ~/.aws, ~/.kube, ~/.gnupg Skip and error Never; keeps the individual file Refused without --force-sensitive-collapse
Guarded /etc, /proc, /sys, /dev, /boot, /run/secrets Emit, warn, and diff Never; keeps the individual file Refused without --force-sensitive-collapse
Normal Everything else Collapse freely Collapse freely Collapse freely

The observed-versus-granted diff

When a write collapse lands on a guarded path, learn prints a warning to stderr along with a diff: the list of siblings in that directory the workload never touched but will now have write access to. That difference is the cost of the collapse, made explicit so an operator can decide whether it is acceptable rather than discovering it later.

--force-sensitive-collapse permits --collapse-prefix to target protected and guarded paths. The warning and the diff are still printed.

Examples

Observe, then run under the result
# Observe a Python script and generate a profile
$ sandlock learn -o profile.toml -- python3 build.py

# Run under the generated profile
$ sandlock run -p profile.toml -- python3 build.py

# Collapse common library directories for a tighter profile
$ sandlock learn --collapse -o profile.toml -- python3 build.py

A generated profile is a starting point, not a finished policy. It describes one execution. A code path that did not run contributes nothing, so the first real run under the profile may still be missing a path. Review what it produced, tighten what is too broad, and exercise the workload's less common paths before trusting it in production.

Inspecting a running sandbox

sandlock inspect prints a live sandbox's effective policy, which is the other direction of the same idea: rather than generating a policy from a run, it recovers the policy a run is actually operating under. TOML output round-trips into a profile.

Recover a policy from a running sandbox
$ sandlock ps
NAME          PID      UPTIME  CMD
api.local     12345        5m  python3 server.py

$ sandlock inspect api.local            # JSON
$ sandlock inspect api.local --toml     # TOML

A sandbox is only visible to ps and inspect when its control socket is enabled, which it is by default. Setting control_socket=False creates no runtime directory, pid file, or control-socket task at all, and the sandbox becomes invisible to both.