The policy file
One file at the project root. Every command finds it by walking up from the working directory, the same way git finds .git.
protect: # paths relative to this file - .env # a file - config # a directory, and everything under it - config/** # the same thing, spelled out - src/*.ts # * stops at / - "**/secrets.json" # ** does not
agent.lockalways protects itself, whether or not you list it..., absolute paths,~and!negation are rejected at parse time rather than quietly reinterpreted. A policy that does not mean what it says is worse than no policy.- Unknown keys are errors, so
protects:fails loudly instead of parsing as a policy that happens to protect nothing. version:is optional and means1. Files that state it still work andversion: 2is still refused — it just was not earning its line, since "no version stated" says version one perfectly well and always will.- An empty
agent.lockis refused rather than treated as a policy with nothing in it. "Enforced, protecting nothing" is the one status that must never be reassuring, andtouch agent.lockis an easy way to reach it.
Patterns
| Pattern | Matches | Does not match |
|---|---|---|
.env | that exact file | src/.env |
config | the directory and everything beneath it, at any depth | configuration |
config/** | identical to the above — the directory itself is included, so it cannot be renamed out from under the policy | — |
src/*.ts | src/auth.ts | src/deep/auth.ts — * stops at a separator |
**/secrets.json | the file at any depth, including the root | secrets.json.bak |
Matching is case-sensitive on Linux and case-insensitive on Windows and macOS. On a case-insensitive filesystem, a deny list that matched fewer paths than the filesystem does would be wrong in the dangerous direction.
Commands
| Command | Does |
|---|---|
ralon install | Registers the per-user background supervisor — a Task Scheduler logon task on Windows, a launchd LaunchAgent on macOS. Additive: re-running it never drops a scope. Fails on Linux, with the reason. --scope names a directory, --here covers only the project you are standing in, --no-hooks skips configuring agents, --dry-run registers nothing. |
ralon scope add|list|remove | The directories a policy is honoured in. Where Ralon is installed does not decide this — a home directory on C: says nothing about a repository on D:. Scopes are kept disjoint and canonical; add and remove reconcile before returning. A running supervisor holds the scope file against writers, so these commands ask it to stand down, write, and start it again — the guards keep holding their projects throughout. |
ralon pause / resume | Releases one project so its own policy can be edited, because agent.lock protects itself. Expires after fifteen minutes unless --indefinitely is given. |
ralon uninstall | Deregisters the supervisor, releases every project it held, and removes its copy of the binary. --keep-enforcement leaves the enforcement in place with nothing watching it. Run this before removing the ralon package — no package manager can do it for you. |
ralon status | The policy, the protected paths on disk, the backends this kernel offers, and three separate answers: is the supervisor registered, is it running, and is this project protected. It also names a registration left pointing at a binary that no longer exists — the state a machine reaches by removing the ralon package without deregistering first. |
ralon check [paths...] | Reports whether the given paths are protected, exiting 1 if any are. With no arguments, lists everything the policy protects right now. |
ralon run -- <cmd> | Restricts the current process and replaces it with the command. --dry-run prints the plan without enforcing it; --backend pins the choice; --quiet drops the summary line. |
ralon guard | One project's enforcement, held with no command to supervise — what the supervisor starts, by hand, and the way to protect a repository without installing anything. --detach backgrounds it, --stop releases it. The claim is machine-wide, so a guard the supervisor started is visible to and stoppable from your own terminal. |
ralon init | Writes a starter agent.lock and configures the agents. Refuses to overwrite a policy without --force. Not needed under a supervisor: writing the file by hand is the whole step. |
ralon hook install | Writes the refusal into nine agents' own configuration, so a blocked write reads as "protected by Ralon" rather than EBUSY. Done automatically for each project the supervisor enforces. |
ralon daemon | The supervisor itself, started by the operating system rather than by people. --once does a single pass and prints what changed. |
Every command takes --dir to point at a project other than the working directory. Supervisor state — the scopes, the recorded workspaces, the log, and Ralon's own copy of the binary — lives in %LOCALAPPDATA%\Ralon or ~/Library/Application Support/Ralon, relocatable with RALON_HOME.
The copy is why install registers a path of its own rather than wherever the executable happened to be. Most installs put that inside a package manager's directory, and Windows will not delete the image of a running process — so a supervisor running from node_modules made its own package impossible to uninstall, with an error about permissions rather than about a running process.
Exit codes
These are the interface. A wrapper that swallows them reports every policy as satisfied.
| Code | Meaning |
|---|---|
0 | Fine. |
1 | A path is protected (check), or the plan cannot be enforced here (--dry-run). |
2 | Error: no policy, a bad policy, no usable backend, or the command failed to start. |
Otherwise run exits with your command's own status, because it is your command by then.
Backends
run picks the strongest backend the kernel offers. ralon status shows what is available and --backend pins it.
| Backend | Platform | Mechanism |
|---|---|---|
mount | Linux | read-only bind mounts in a locked namespace |
landlock | Linux | the kernel LSM, where namespaces are unavailable |
seatbelt | macOS | the policy compiled to SBPL and applied with sandbox_init |
immutable | macOS | chflags uchg — what the supervisor uses, and a narrowing rather than a sandbox |
locks | Windows | exclusive share-mode handles, refused to every process |
The first three are inherited — applied to a process before it starts and impossible to impose on one already running, which is why run exists and why Linux has no supervisor. The last two are imposed — held by one process and refused to everybody else — which is why a guard can protect agents it did not start.
mount — the default
Read-only bind mounts inside a user and mount namespace, locked by entering a second namespace so they cannot be undone. Every parent directory of a protected path becomes a mount point too, so no directory on the way to it can be renamed or removed. Precise: nothing outside the protected paths behaves differently. Needs unprivileged user namespaces, which some hardened distributions and container runtimes disable.
landlock — the fallback
The kernel LSM, Linux 5.13+. Needs no namespaces, so it works exactly where the mount backend cannot. Landlock rules are additive — a rule may grant more access than its parents, never less — so "everything except this file" has to be expressed by granting every sibling along the way instead.
The consequence is visible and worth knowing: directories leading to a protected path stop accepting new entries. With src/index.tsx protected, everything in src/ and the project root stays writable, but new files cannot be created directly in either — inside tests/ or any other subtree they are fine. ralon run --dry-run --backend landlock lists exactly which directories are affected.
seatbelt — macOS, under run
The policy compiled to SBPL and applied with sandbox_init before the command starts. The only mechanism here that can say deny outright, so it needs no carve-out and no ACL: a protected directory covers whatever is created in it later, and ancestors are denied as literal nodes so none of them can be renamed while staying writable inside.
immutable — macOS, under the supervisor
chflags uchg on each protected path. It is the only way to impose a restriction on a process you did not start without asking for root, and it is honestly weaker than the other four: an agent undoes it with chflags nouchg, and it cannot pin an ancestor without also freezing it, so renaming an unprotected parent directory moves the path out from under the policy. ralon run has neither limitation, and ralon status warns when a policy has an ancestor exposed this way.
locks — Windows
Exclusive share-mode handles on every protected path, held for as long as Ralon holds them and refused to every process on the machine — no cooperation required from the thing being refused. Directories are narrowed with an ACL as well, because a handle on a directory does not stop files being created inside it. The handles are a property of the file object rather than of a session, so a guard started by the supervisor in the background is refusing writes from your terminal too.