# Filesystem region model (minimal, post-retreat).
#
# safe-chains classifies COMMANDS; it is deliberately NOT a filesystem trust database. Modeling
# "what every path on every machine means" is unbounded, per-machine, and drifts — and an
# adversarial audit of the old broad admit map (public /etc, /proc, /usr, /var/log, /Library)
# found it leaking (macOS keychain, Homebrew service configs, auth logs). Filesystem enforcement
# belongs to a sandbox / the OS / the harness, not a static string classifier (same scope stance
# as $PATH and hostile checkouts — see AGENTS.md §0.2).
#
# So this file is TINY and does only what a static classifier can honestly do:
# • the WORKSPACE (relative paths under the harness cwd) is read/write — the agent was invited
# there. That's the default `worktree` role; no node needed.
# • a few `.git`/`.envrc`-style in-project files are read-safe but WRITE-frozen (a hook/direnv
# write is code-injection).
# • scratch (`/tmp`) and standard streams (`/dev/null`, fd pseudo-files) are read/write.
# • a small CREDENTIAL SHIELD (`.ssh`, `.aws`, …) is denied and — crucially — a user grant can
# NEVER widen it (see `apply_grant`), so `grant ~/` still can't hand over an SSH key.
# • WRITES stop at the workspace, its siblings and scratch. Everything else is `unknown`, whose
# write face is `machine` — above where writes reach — so it denies and the harness prompts.
#
# READS are the asymmetry, and it is deliberate. They run to the machine rung: `/etc/hosts`,
# `~/.zshrc`, `/usr/bin/python3`, a sibling checkout and a vendored crate source all read without
# a prompt, because confining reads to the worktree cost a prompt on nearly every legitimate thing
# an agent does and bought protection only for files that mostly are not secret. What bounds a
# read is therefore this file's shield — a test on the NAME — plus three rungs that sit ABOVE
# machine because they are not ordinary files at all: `device` (raw disks and memory),
# `system-integrity` (the trust substrate) and `kernel`.
#
# The shield being a name test is what forces the rest of the model. A name test can only answer
# about a path someone actually wrote down, so anything that reads files it never names has to be
# refused wholesale above the workspace: an unbounded sweep (`grep -r foo ~`), a glob (`/etc/*`), a
# traversal's placeholder (`find ~ -exec cat {}`) and an unpinnable path (`$SECRET`, a `..` with no
# cwd to resolve against). Those are not special cases bolted on; they are the same rule.
#
# Matching (header rules unchanged): a `path` ending `/` = SUBTREE prefix; ending `*` = STRING
# prefix; no `/` = SEGMENT (matches any path component == it, at any depth); else EXACT. Most
# specific wins; fail-closed to `unknown`. Credential shields are SEGMENTS so they bite whatever
# the spelling (`~/.ssh/id_rsa`, a relative `.ssh/id_rsa`, a `.ssh/` inside a granted tree).
#
# PROVENANCE: every node keeps a `note` (why it earns its role) and a dated `researched`.
# ── Roles: role → { read face, write face, rebind face, secret, frozen } ───────────────────
#
# THREE faces, not two. `read_locus` and `write_locus` are the obvious ones. `rebind_locus` is the
# locus of an operation that changes what the NAME refers to rather than the bytes under it: `rm`
# and `rmdir` unbind it, `ln` points it elsewhere, `mv` takes it away. It defaults to the write
# face and only needs stating where the two must differ.
#
# `frozen` says which face a user GRANT may not widen:
# (absent) a grant widens every face
# frozen = "rebind" a grant may write INTO the node but never replace it
# frozen = "write" a grant may not write it at all, and therefore cannot rebind it either
#
# The distinction is load-bearing rather than decorative. Freezing the WRITE face of `~/.config` to
# stop `ln -s /tmp/evil ~/.config` also denied `cp x ~/.config`, which is an ordinary thing to do in
# a directory you granted. Freezing only the REBIND face keeps both answers right. It cannot be
# derived from the operation, because `cp x DIR` and `ln -s y DIR` are both `create`/`transfer` to
# the engine — `ln` declares `rebinds_destination` in its own TOML to separate them.
[role.worktree]
description = "The project tree (relative paths under the workspace). Read + write."
read_locus = "worktree"
write_locus = "worktree"
reads_secret = false
[role.worktree-trusted]
description = "In-project files another tool auto-executes (.git internals, .envrc). Read-safe; writes frozen (a hook/config write is code-injection)."
read_locus = "worktree-trusted"
write_locus = "worktree-trusted"
reads_secret = false
[role.scratch]
description = "Process-scoped temporary scratch."
read_locus = "temp"
write_locus = "temp"
reads_secret = false
[role.standard-stream]
description = "Standard streams / fd pseudo-files — no real filesystem is touched."
read_locus = "process"
write_locus = "process"
reads_secret = false
[role.raw-device]
description = "Raw block/char devices. Reading one is not reading a file — `/dev/rdisk0` is the whole disk and `/dev/mem` is physical memory, so both hand over every secret on the host in one operand. Sits at the `device` rung on both faces, which is ABOVE the machine rung ordinary local reads reach, so it stays refused when reads open up without needing to be enumerated as a secret."
read_locus = "device"
write_locus = "device"
reads_secret = false
[role.credential-store]
description = "Known credential/secret stores. Denied, and a user grant may NOT widen them (the shield)."
read_locus = "machine"
write_locus = "machine"
reads_secret = true
[role.safe-chains-config]
description = "safe-chains' own config. Read-OK, but write DENIED and un-grantable — an agent must not rewrite the file that governs what it may do. The write face sits at `system-integrity`, not `machine`: this file IS the classifier's trust substrate, and a level that admits ordinary machine administration must not thereby admit rewriting its own ceiling."
read_locus = "worktree-trusted"
write_locus = "system-integrity"
reads_secret = false
frozen = "write"
[role.trust-source]
description = "A file safe-chains derives its own trust from: a harness settings file it reads allow-rules or read-approvals out of. Write DENIED and un-grantable — an agent that rewrites one of these decides what safe-chains approves next. Unlike safe-chains' own config the READ face stays machine, because a harness settings file can carry an `env` block with API keys. Its write face sits at `system-integrity` for the same reason safe-chains own config does: a level admitting ordinary machine administration must not thereby admit rewriting the file that decides what gets approved next."
read_locus = "machine"
write_locus = "system-integrity"
reads_secret = false
frozen = "write"
[role.trust-root-dir]
description = "The DIRECTORY a trust file lives in. Writing a file into it is ordinary and stays grantable; REBINDING it is not, because replacing the directory points the trust root at somewhere else entirely and every file pin beneath it becomes irrelevant. A grant on such a directory therefore permits `cp x ~/.config` and refuses `rm -rf ~/.config` and `ln -s /tmp/evil ~/.config`. The REBIND face sits at `system-integrity` so the freeze survives a raised level, while the write face stays at `machine` and remains grantable."
read_locus = "machine"
write_locus = "machine"
rebind_locus = "system-integrity"
reads_secret = false
frozen = "rebind"
[role.unknown]
description = "Fail-closed default: any absolute/home path matching no region. Deny read & write (the harness then prompts)."
read_locus = "machine"
write_locus = "machine"
reads_secret = false
# ── In-project trust carve-outs: read-safe, write-frozen (code-injection) ───────────────────
[[region]]
path = ".git"
role = "worktree-trusted"
note = "Git metadata dir; hooks/ and config execute code on git operations, so a write is code-injection. Nests at any depth."
researched = "2026-07-12"
[[region]]
path = ".envrc"
role = "worktree-trusted"
note = "direnv auto-executes .envrc on cd into its directory; a write plants code that runs on the next shell entry."
researched = "2026-07-12"
# ── Scratch + standard streams: read/write ─────────────────────────────────────────────────
[[region]]
path = "/tmp/"
role = "scratch"
note = "FHS world-writable temp; ephemeral scratch. Both OSes (macOS /tmp symlinks to /private/tmp)."
researched = "2026-07-12"
[[region]]
path = "/var/tmp/"
role = "scratch"
note = "FHS temp preserved across reboots; still process-throwaway in intent."
researched = "2026-07-12"
[[region]]
path = "/private/tmp/"
role = "scratch"
os = ["macos"]
note = "macOS's real temp directory; /tmp is a symlink to here."
researched = "2026-07-12"
[[region]]
path = "/private/var/tmp/"
role = "scratch"
os = ["macos"]
note = "macOS real location of /var/tmp."
researched = "2026-07-12"
[[region]]
path = "/dev/null"
role = "standard-stream"
note = "POSIX bit-bucket; discards writes, empty on read."
researched = "2026-07-12"
[[region]]
path = "/dev/stdout"
role = "standard-stream"
note = "POSIX stdout pseudo-file — the process's own output stream."
researched = "2026-07-12"
[[region]]
path = "/dev/stderr"
role = "standard-stream"
note = "POSIX stderr pseudo-file — the process's own error stream."
researched = "2026-07-12"
[[region]]
path = "/dev/tty"
role = "standard-stream"
note = "The controlling terminal; process-local I/O."
researched = "2026-07-12"
[[region]]
path = "/dev/fd/"
role = "standard-stream"
note = "Per-process file-descriptor pseudo-files (process substitution). Process-local."
researched = "2026-07-12"
[[region]]
path = "/dev/"
role = "raw-device"
note = """
Everything else under /dev is a raw device until shown otherwise. The standard streams above are
Exact nodes and outrank this subtree, so they keep reading and writing normally.
Positive classification rather than a list of dangerous device names: `/dev/mem`, `/dev/kmem`,
`/dev/rdisk0`, `/dev/sda`, `/dev/nvme0n1` and every future spelling land here by default, and a
device that turns out to be ordinary can be given its own node. Enumerating the dangerous ones
would have to be complete to be worth anything.
"""
researched = "2026-08-12"
[[region]]
path = "/proc/*/environ"
role = "credential-store"
os = ["linux"]
note = "A process's environment block — where an agent's own API tokens and cloud keys live. Per-pid, so the component is a wildcard; /proc/self/environ matches it too."
researched = "2026-08-12"
[[region]]
path = "/proc/*/mem"
role = "credential-store"
os = ["linux"]
note = "A process's address space: every secret it has decrypted or been handed, readable as a file."
researched = "2026-08-12"
[[region]]
path = "/proc/*/cmdline"
role = "credential-store"
os = ["linux"]
note = "A process's argv, which routinely carries tokens passed as flags (`--password`, `--token`)."
researched = "2026-08-12"
# NOT shielded: safe-chains' own decision log at `~/.local/state/safe-chains/`.
#
# It was, for one release. The reasoning — it records command lines verbatim, so it accumulates
# whatever secrets appeared on one — is true and was the wrong call anyway. Logging is entirely
# opt-in. Someone who turns it on has already weighed that trade and did it in order to READ the
# result, so shielding the file removes the only reason it exists. Every question asked of it ("why
# was this denied", "what is prompting most") needs a manual approval, which is the feature
# defeating itself.
#
# The general rule stands and covers this: an ordinary file under `~` reads, a credential store
# does not. The log is not a credential store — it is a log the user asked for.
[[region]]
path = "/var/log/auth.log"
role = "credential-store"
os = ["linux"]
note = "Authentication log — sudo/su/ssh events, and passwords typed into a username prompt land here verbatim."
researched = "2026-08-12"
[[region]]
path = "/var/lib/mysql/"
role = "credential-store"
os = ["linux"]
note = "MySQL/MariaDB data directory. The `mysql.user` table lives here as a file, so the password hashes for every database account are readable straight off disk."
researched = "2026-08-12"
[[region]]
path = "/var/lib/postgresql/"
role = "credential-store"
os = ["linux"]
note = "PostgreSQL data directory — `pg_authid` carries the role password hashes, and pgpass/certs are commonly kept alongside."
researched = "2026-08-12"
[[region]]
path = "/var/log/secure"
role = "credential-store"
os = ["linux"]
note = "The RHEL/Fedora spelling of auth.log."
researched = "2026-08-12"
# ── Package CONTENT: read-admitted, writes still denied. ───────────────────────────────────────
#
# The retreat deleted a broad admit map because an adversarial audit found it leaking — macOS
# `/Library/Keychains/System.keychain`, Homebrew `/usr/local/etc/*` service configs, `/var/log/*`
# auth tokens. That map failed because it admitted whole ROOTS, and a root is HETEROGENEOUS:
# `/usr/local` holds distributed package files next to machine-local config and state.
#
# These nodes cut at the layer the FHS already separates. `share`/`include`/`lib` is content a
# PACKAGE MANAGER put there; `etc` and `var` are this machine's config and state, which is where
# every audited leak lived. None of the three audit findings matches a prefix below: `/Library`
# is admitted only at `Developer/CommandLineTools/`, `etc` is never admitted at any root, and
# `/var` is not a root here at all.
#
# The argument for reading these at all: the content is public by construction. A crate README
# under `~/.cargo/registry` is published on crates.io; a man page ships in the OS. Refusing the
# local copy while the same bytes are a network fetch away is friction without a matching risk.
# WRITES stay denied — this widens disclosure only, not the ability to alter what runs.
[[region]]
path = ".ssh"
role = "credential-store"
note = "SSH private keys / authorized_keys / known_hosts — credential + trust store."
researched = "2026-07-12"
[[region]]
path = ".aws"
role = "credential-store"
note = "AWS access keys / session credentials."
researched = "2026-07-12"
[[region]]
path = ".gnupg"
role = "credential-store"
note = "GnuPG private keyring and trust database."
researched = "2026-07-12"
[[region]]
path = ".kube"
role = "credential-store"
note = "Kubernetes kubeconfig — cluster credentials and tokens."
researched = "2026-07-12"
[[region]]
path = ".docker"
role = "credential-store"
note = "Docker registry auth tokens / credential-helper config."
researched = "2026-07-12"
[[region]]
path = ".netrc"
role = "credential-store"
note = "Plaintext machine login credentials for ftp/curl/git."
researched = "2026-07-12"
# ── Credential DOTFILES. A prerequisite for admitting home dotfiles at all, not a follow-up. ──
#
# Home dotfiles are simultaneously the most ordinary read on the disk (`.zshrc`, `.gitconfig`) and
# the densest concentration of plaintext credentials on it. `home_role` currently excludes any hidden
# component for that reason — but measured, that exclusion protects nothing once local reads open:
# an excluded dotfile falls through to `unknown` → `machine`, and `machine` reads are exactly what
# the read policy admits. `cat ~/.git-credentials` ALLOWS with the cap lifted. These nodes are the
# only thing that will actually stand in front of them.
#
# SEGMENT matches for the bare filenames, so a project-local copy is shielded too — a repo `.npmrc`
# carries the same `_authToken` as the home one. Over-denying an innocent one is the accepted trade
# the shield header already states.
[[region]]
path = ".git-credentials"
role = "credential-store"
note = "git-credential-store's default file: one plaintext `https://user:pass@host` per line. Its XDG twin `~/.config/git/credentials` is already covered by the `~/.config/git/` node. Verified: git-scm.com/docs/git-credential-store."
researched = "2026-08-15"
[[region]]
path = ".npmrc"
role = "credential-store"
note = "npm config; holds `//registry.example/:_authToken=…` registry tokens. Segment-matched so a project-local .npmrc is shielded too — it carries the same token."
researched = "2026-08-15"
[[region]]
path = ".pypirc"
role = "credential-store"
note = "PyPI upload credentials — `username`/`password` or an API token per repository section."
researched = "2026-08-15"
[[region]]
path = ".pgpass"
role = "credential-store"
note = "libpq password file, `host:port:db:user:password` per line. Postgres refuses it unless mode 0600, which is the tell."
researched = "2026-08-15"
[[region]]
path = ".my.cnf"
role = "credential-store"
note = "MySQL client config; `[client] password=` is the documented way to avoid typing it."
researched = "2026-08-15"
[[region]]
path = ".dockercfg"
role = "credential-store"
note = "The pre-1.7 Docker registry auth file (base64 user:pass). Superseded by ~/.docker/config.json, which the `.docker` segment already shields, but still read by older tooling."
researched = "2026-08-15"
[[region]]
path = ".terraform.d"
role = "credential-store"
note = "Holds credentials.tfrc.json — the Terraform Cloud/Enterprise API token. A SEGMENT, like `.ssh`/`.aws`: written with a trailing slash it became a subtree PREFIX anchored at the start of the path, which matched nothing real and let `$(fd pat /work)/.terraform.d` out-permit the literal `/work/.terraform.d` — caught by substitution_is_never_more_permissive_than_a_path_it_could_produce."
researched = "2026-08-15"
[[region]]
path = "~/.azure/"
role = "credential-store"
note = "Azure CLI token cache (msal_token_cache.json / accessTokens.json) and the active subscription profile."
researched = "2026-08-15"
[[region]]
path = "~/.cargo/credentials"
role = "credential-store"
note = "crates.io registry token. Cargo writes `credentials.toml` since 1.68 but still reads and prefers the extensionless name when both exist, so BOTH are shielded. Deliberately exact paths, not a `.cargo` segment: the sibling `~/.cargo/registry/` is published crate source and stays readable. Verified: doc.rust-lang.org/cargo/reference/config.html."
researched = "2026-08-15"
[[region]]
path = "~/.cargo/credentials.toml"
role = "credential-store"
note = "As above — the current default spelling of the crates.io token file."
researched = "2026-08-15"
[[region]]
path = "~/.gem/credentials"
role = "credential-store"
note = "RubyGems `:rubygems_api_key:` — the push token."
researched = "2026-08-15"
[[region]]
path = "~/.m2/settings.xml"
role = "credential-store"
note = "Maven `<server><password>` entries for deploy repositories."
researched = "2026-08-15"
[[region]]
path = "~/.gradle/gradle.properties"
role = "credential-store"
note = "Conventional home for signing keys and repository credentials (`signing.password`, `*.apiKey`)."
researched = "2026-08-15"
[[region]]
path = "~/.composer/auth.json"
role = "credential-store"
note = "Composer HTTP-basic and OAuth tokens for private PHP package repositories."
researched = "2026-08-15"
# ── Second pass. The first list was assembled from memory and an adversarial probe found ten more
# readable in one go — including `.boto`, which the accompanying TODO note had already WRITTEN
# DOWN as missing. Recorded because it is the measurement that matters about this approach: the
# shield is an enumeration, and an enumeration of "config files that hold credentials" trails the
# ecosystem permanently. See TODO.md for the structural question that raises.
[[region]]
path = ".boto"
role = "credential-store"
note = "Legacy boto/gsutil config; holds aws_access_key_id / aws_secret_access_key and gs_oauth2_refresh_token."
researched = "2026-08-15"
[[region]]
path = ".vault-token"
role = "credential-store"
note = "HashiCorp Vault CLI token, written bare into the file by `vault login`. A live credential to everything the token can reach."
researched = "2026-08-15"
[[region]]
path = ".databrickscfg"
role = "credential-store"
note = "Databricks CLI profiles; `token = dapi…` personal access tokens."
researched = "2026-08-15"
[[region]]
path = ".authinfo"
role = "credential-store"
note = "Emacs/GNU auth-source netrc-format store — the `.netrc` of the Emacs world, same plaintext shape. (`.authinfo.gpg` is encrypted but shielded by the same segment.)"
researched = "2026-08-15"
[[region]]
path = "~/.pip/pip.conf"
role = "credential-store"
note = "pip config; `index-url = https://user:pass@host/simple` is the documented way to reach a private index."
researched = "2026-08-15"
[[region]]
path = "~/.kaggle/"
role = "credential-store"
note = "kaggle.json — username plus API key."
researched = "2026-08-15"
[[region]]
path = "~/.subversion/auth/"
role = "credential-store"
note = "Subversion's cached credentials; the simple/ provider stores passwords in cleartext."
researched = "2026-08-15"
[[region]]
path = "~/.oci/"
role = "credential-store"
note = "Oracle Cloud CLI config plus the API signing private key it points at."
researched = "2026-08-15"
[[region]]
path = "~/.snowflake/"
role = "credential-store"
note = "Snowflake CLI connections.toml — account passwords and private-key paths."
researched = "2026-08-15"
[[region]]
path = ".hgrc"
role = "credential-store"
note = "Mercurial config; the `[auth]` section holds per-prefix usernames and passwords in cleartext."
researched = "2026-08-15"
# ── System credential stores + other-user homes: absolute paths a broad grant (`grant /` or
# `grant /etc/`) would otherwise expose. Kept as un-grantable shields (the dotfile rule only
# covers hidden entries; these are not dotfiles). ──────────────────────────────────────────
[[region]]
path = "/etc/shadow"
role = "credential-store"
os = ["linux"]
note = "Hashed user passwords — the credential store proper."
researched = "2026-07-12"
[[region]]
path = "/etc/gshadow"
role = "credential-store"
os = ["linux"]
note = "Hashed group passwords."
researched = "2026-07-12"
[[region]]
path = "/etc/master.passwd"
role = "credential-store"
os = ["macos"]
note = "macOS password hash store."
researched = "2026-07-12"
[[region]]
path = "/etc/ssh/"
role = "credential-store"
note = """
Host private keys (`ssh_host_*_key`) — impersonating the host is exactly what they enable. The
whole directory, not the key files: node matching has no globs, and the readable half (sshd_config,
known_hosts) is worth little enough that shielding it costs nothing next to naming five key
patterns and missing the sixth.
"""
researched = "2026-08-12"
[[region]]
path = "/etc/ssl/private/"
role = "credential-store"
note = "TLS/service private keys."
researched = "2026-07-12"
[[region]]
path = "/root/"
role = "credential-store"
os = ["linux"]
note = "root's home — privileged private data + credentials."
researched = "2026-07-12"
[[region]]
path = "/var/root/"
role = "credential-store"
os = ["macos"]
note = "macOS root home."
researched = "2026-07-12"
# Well-known credential stores NESTED under ~/.config (non-hidden, so the dotfile rule can't
# catch them under an explicit `grant ~/.config/`). Not exhaustive — an explicit grant of a
# config dir can still expose an unlisted app's secrets; the docs advise granting narrowly.
[[region]]
path = "~/.config/gh/"
role = "credential-store"
note = "GitHub CLI OAuth token (hosts.yml)."
researched = "2026-07-12"
[[region]]
path = "~/.config/gcloud/"
role = "credential-store"
note = "Google Cloud SDK credentials / tokens."
researched = "2026-07-12"
[[region]]
path = "~/.config/rclone/"
role = "credential-store"
note = "rclone remote credentials (rclone.conf)."
researched = "2026-07-12"
[[region]]
path = "~/.config/git/"
role = "credential-store"
note = "git's `store` credential helper — plaintext https://user:token@host."
researched = "2026-07-12"
# ── macOS ~/Library credential stores. The dotfile rule keeps a broad `grant ~/` from widening
# HIDDEN entries, but on macOS the keychain, browser password stores, cookies, and the iMessage
# DB live under ~/Library — NOT dot-prefixed — so a broad grant would otherwise sweep them up.
# Shielded (un-grantable) like the credential dotdirs. Not exhaustive (Application Support holds
# countless app secrets); the docs advise granting narrowly. ─────────────────────────────────
[[region]]
path = "~/Library/Keychains/"
role = "credential-store"
os = ["macos"]
note = "macOS keychain databases (login.keychain-db)."
researched = "2026-07-12"
[[region]]
path = "~/Library/Cookies/"
role = "credential-store"
os = ["macos"]
note = "Per-app HTTP cookie stores (session tokens)."
researched = "2026-07-12"
[[region]]
path = "~/Library/Safari/"
role = "credential-store"
os = ["macos"]
note = "Safari history / autofill / per-site data."
researched = "2026-07-12"
[[region]]
path = "~/Library/Messages/"
role = "credential-store"
os = ["macos"]
note = "iMessage database (chat.db) — private message history."
researched = "2026-07-12"
[[region]]
path = "~/Library/Application Support/Firefox/"
role = "credential-store"
os = ["macos"]
note = "Firefox profiles: logins.json + key4.db (offline-decryptable saved passwords)."
researched = "2026-07-12"
[[region]]
path = "~/Library/Application Support/Google/Chrome/"
role = "credential-store"
os = ["macos"]
note = "Chrome profile: Login Data / Cookies stores."
researched = "2026-07-12"
[[region]]
path = "~/Library/Application Support/Chromium/"
role = "credential-store"
os = ["macos"]
note = "Chromium profile credential/cookie stores."
researched = "2026-07-12"
[[region]]
path = "~/Library/Application Support/BraveSoftware/"
role = "credential-store"
os = ["macos"]
note = "Brave profile credential/cookie stores."
researched = "2026-07-12"
[[region]]
path = "~/Library/Application Support/Microsoft Edge/"
role = "credential-store"
os = ["macos"]
note = "Edge profile credential/cookie stores."
researched = "2026-07-12"
# ── safe-chains' own config: read-OK, write-DENIED, un-grantable (the trust root). An
# UNPROTECTED path (no harness blesses a third-party config location) — a stopgap until one
# exists; a write here is best-effort caught (a python/editor write escapes the command
# classifier). This is the ONLY location safe-chains reads its config from — `XDG_CONFIG_HOME`
# is not honored (an agent-mutable env var must not relocate the trust root). See the
# protected-config research item. ────────────────────────────────────────────────────────────
[[region]]
path = "~/.config/safe-chains.toml"
role = "safe-chains-config"
note = "safe-chains' user config (grants). Writing it would let an agent grant itself permissions."
researched = "2026-07-12"
[[region]]
path = "~/.claude/settings.json"
role = "trust-source"
note = "safe-chains reads `permissions.allow` command patterns AND Read() path approvals out of this file (src/allowlist.rs, regions.rs::claude_settings_read_grants), and the hook gates on the coverage those patterns produce. Writing it is therefore a way to decide what safe-chains approves next, exactly like writing safe-chains' own config."
researched = "2026-08-02"
# ── Trust-root DIRECTORIES. Freezing the trust FILES is not enough on its own: a grant on the
# parent let an agent delete the directory and replace it with a symlink to one it controls,
# after which safe-chains read its grants (and its `level` ceiling) from the agent's own file.
# Every step of that was auto-approved. safe-chains classifies a path by its literal spelling and
# does not follow symlinks (AGENTS.md §0.2), which is right for classification but means the
# RELOCATION has to be stopped where it happens, at the point the directory is replaced.
#
# EXACT matchers, so only the directory node itself is frozen. Files underneath are untouched and
# a `~/.config` grant still works normally for every other tool's config living there.
# `/etc` needs no equivalent: it already denies as `machine`.
[[region]]
path = "~/.config"
role = "trust-root-dir"
note = "Parent of safe-chains' own config. `rm -rf ~/.config` then `ln -s /tmp/evil ~/.config` relocated the whole trust root."
researched = "2026-08-02"
[[region]]
path = "~/.claude"
role = "trust-root-dir"
note = "Parent of the harness settings file safe-chains reads allow-rules from. Same relocation."
researched = "2026-08-02"
# ── System-integrity substrate: the machine's own identity/auth/boot/loader files, where a WRITE is
# compromise-complete — add a UID-0 user, grant yourself sudo, rewrite the auth/login path, redirect
# the dynamic loader, or change the boot path. The READ face is ordinary machine config; the WRITE
# face worst-cases to `system-integrity`, which sits ABOVE `machine` (and above local-admin), so
# owning the trust substrate is a yolo-level grant — distinct from ordinary machine admin (a service
# restart, an app-config edit). Deny-ward and fail-closed like the credential shield above: this is
# NOT an admit map (the auto-approve band already denies all of `machine`); it only splits the
# upper-level bands so "run the machine as admin" (local-admin) and "own the machine's trust root"
# (yolo) are different grants. The secret-READ stores (/etc/shadow, master.passwd) stay in the
# credential shield; these are the write-compromise files that are not themselves secrets. ────────
[role.system-integrity]
description = "The machine's identity/auth/boot/loader substrate. Readable as ordinary config; a WRITE is compromise-complete and worst-cases above local-admin (system-integrity)."
read_locus = "machine"
write_locus = "system-integrity"
reads_secret = false
[[region]]
path = "/etc/passwd"
role = "system-integrity"
note = "System account database; a write can add a UID-0 user — full compromise."
researched = "2026-07-16"
[[region]]
path = "/etc/group"
role = "system-integrity"
note = "System group database; a write can grant membership in privileged groups (wheel/sudo/admin)."
researched = "2026-07-16"
[[region]]
path = "/etc/sudoers"
role = "system-integrity"
note = "sudo policy; a write grants arbitrary root — the ultimate escalation."
researched = "2026-07-16"
[[region]]
path = "/etc/sudoers.d/"
role = "system-integrity"
note = "Drop-in sudo policy fragments; same grant-root power as /etc/sudoers."
researched = "2026-07-16"
[[region]]
path = "/etc/pam.d/"
role = "system-integrity"
note = "PAM authentication stack (Linux and macOS); a write rewrites how every login authenticates."
researched = "2026-07-16"
[[region]]
path = "/etc/ld.so.conf"
role = "system-integrity"
os = ["linux"]
note = "Dynamic-loader search config; a write can redirect every program to attacker libraries."
researched = "2026-07-16"
[[region]]
path = "/etc/ld.so.conf.d/"
role = "system-integrity"
os = ["linux"]
note = "Dynamic-loader search drop-ins; same library-hijack power as ld.so.conf."
researched = "2026-07-16"
[[region]]
path = "/etc/ld.so.preload"
role = "system-integrity"
os = ["linux"]
note = "Force-loaded shared objects for every dynamically-linked program — a machine-wide code-injection hook."
researched = "2026-07-16"
[[region]]
path = "/boot/"
role = "system-integrity"
os = ["linux"]
note = "Kernel images, initramfs, and bootloader config; a write changes what the machine boots."
researched = "2026-07-16"