Configuration
Config is TOML. It cascades, so shared rules live high up and specific rules live next to the thing they apply to.
Layers
From lowest precedence to highest:
- The bundled profile.
--profile <name>, defaultuntrusted. Theuntrustedfloor is always applied; a named profile layers over it. - Global config.
$XDG_CONFIG_HOME/bailey/config.toml, falling back to~/.config/bailey/config.toml. - Per-directory config. Every
bailey.tomlfound by walking up from the target's directory and from your working directory, ordered by path depth so a closer file wins. - Explicit config. A file passed with
--config.
Missing files are skipped. bailey show <target> lists the ones that were actually found, in order, and says which walk found each.
Both walks matter
The target walk covers a program that lives with its config. The working directory walk covers the interpreter case: for bailey run /usr/bin/python3 app.py the target walk starts at /usr/bin and finds nothing, while the working directory walk finds your project's bailey.toml. A file found by both contributes once. At equal depth, the working directory wins.
Merge rules
- Filesystem and device grants accumulate. Rights for the same path combine, so a layer granting read and a layer granting write produce read-write.
filesystem.reset = trueclears everything lower layers granted, letting a layer start from nothing.filesystem.denyremoves a grant of the same path from a lower layer and records the path as denied. A later layer that grants the path again overrides the denial.- Scalars are replaced by the nearest layer that sets them:
network.egress, each key under[resources]. network.bind_portsaccumulate.- Hooks accumulate and run in layer order.
- Granting and denying the same path within one layer is an error, naming the file and the path.
A nested deny needs the isolation layer
Denying a path inside a directory another layer granted is enforced by covering that path with an empty read-only filesystem, which only the isolation layer can do. That layer is on by default; under --no-isolate the denial is reported as unenforced and the path stays readable.
Landlock rules cannot express this: access resolution walks up from the file, so an ancestor grant satisfies it and no narrower rule can subtract. See known limitations.
Paths
- Relative paths resolve against the directory of the config file that contains them, not against your shell's working directory.
~and~/...expand to$HOME..and..are collapsed lexically before merging, so different spellings of the same path merge into one grant.
A complete example
[filesystem]
# reset = true
read = ["~/.config/mygame", "./assets"]
write = ["./saves"]
execute = ["./game"]
deny = ["~/.config/mygame/telemetry"]
[network]
egress = "deny" # "deny" (default) or "allow"
egress_allow = [{ host = "*", port = 443 }]
bind_ports = [27015]
[[device]]
path = "/dev/dri"
access = "rw"
[[device]]
path = "/dev/snd"
access = "rw"
[resources]
memory = "4GiB"
pids_max = 512
cpu_percent = 300
[hooks]
pre_launch = ["./mount-assets.sh"]
post_exit = ["./sync-saves.sh"]Every key is documented in the configuration reference.
Network rules
egress = "deny" is the default. egress_allow lists permitted destinations:
[network]
egress_allow = [{ host = "*", port = 443 }]Landlock matches on TCP port, so the host field is advisory. Bailey warns when you set a host other than *, because the rule will not restrict by host. An entry with no port is a resolution error naming the layer, rather than a rule that silently becomes a full deny.
Allowing egress weakens the mode
egress = "deny" puts the target in its own network namespace, where every protocol fails. Allowing any egress, or binding a port, means the target stays in the host's network namespace with only TCP ports enforced, and the run warns that UDP, QUIC, and DNS are unrestricted. See network confinement.
Resource limits
[resources]
memory = "2GiB" # bytes, or a KiB/MiB/GiB or KB/MB/GB suffix
pids_max = 512 # processes and threads
cpu_percent = 150 # percent of one core; 100 is one full coreApplied through cgroup v2, best-effort. Without a writable delegated cgroup they are skipped with a warning.