Ralonv0.1.6

agent.lock · enforced by the kernel

Somefilesarenottheagent'stoedit.

You write the list. Ralon hands it to the kernel — Linux, macOS and Windows — and from that moment the write is refused. Not a matter of the agent's judgement, its prompt, or its mood. Set the machine up once and a repository is protected because it contains the file; there is no second command.

agent.lock
version: 1

protect:
 - src/index.tsx
 - src/auth.ts
 - .env
 - config/**
Same idea as .gitignore, different question. .gitignore says what Git must not track; agent.lock says what must not change.
my-project/
  • agent.locklocked
  • .envlocked
  • src/
  • index.tsxlocked
  • auth.tslocked
  • App.tsxwritable
  • utils.tswritable
  • config/locked
  • tests/writable
agent.lock protects itself. An agent that can rewrite the policy has no policy.
Linux mount namespaces · LandlockmacOS the Seatbelt sandboxWindows exclusive file handlesDepends on nothing
[01]

Try to break it

Pick a platform, then pick an attack. Every one of these is a test in the repository that runs for real against a live sandbox and then checks the file from outside it — because an exit code is not evidence. Note what the Windows column is doing: that is an ordinary cmd that Ralon never started.

ralon run -- shpolicy: src/index.tsx · .env · config/**
$ echo hacked > src/index.tsx
sh: src/index.tsx: Read-only file system
# exit 1 — the file on disk is untouched
refusedmount backend · read-only bind mounts in a locked namespace
[02]

Install

The command is ralon however you install it. Only the crate kept that name: npm refuses it as too similar to an existing package, and PyPI did not have it free, so both use ralonlock.

$ npm install -g ralonlock
# prebuilt binary; npm refuses the name `ralon`

$ ralon --version
ralon 0.1.6

run enforces on all three platforms. ralon status says which mechanism you are getting and why — and if this machine offers none, run refuses to start the command rather than running it unprotected.

[03]

Starting it, on each platform

On Windows and macOS the whole workflow is install once → declare policy → enforcement starts automatically. There is no third step and nothing to run inside a repository: writing an agent.lock is what turns enforcement on, and deleting it is what turns enforcement off. On Linux there is no supervisor, and that is a difference in kernels rather than a rough edge — Linux restrictions are inherited by a process before it runs and cannot be imposed on one already running, so you wrap the agent instead, which is stronger.

Linuxmount namespaces, or Landlock
$ ralon init
$ $EDITOR agent.lock          # say what must not change

$ ralon status
backends
  mount    available (read-only bind mounts in a locked namespace)
  landlock available (kernel ABI v5)

$ ralon run -- claude          # and everything it spawns
ralon: 4 paths locked via the mount backend
Ralon becomes the agent — same terminal, same exit code, same signals, no supervisor process to kill. Two backends: the default builds read-only bind mounts in a namespace it then locks; Landlock takes over where user namespaces are disabled.
macOSthe Seatbelt sandbox
$ ralon init
$ $EDITOR agent.lock

$ ralon run --dry-run           # read the profile before trusting it
(allow default)
(deny file-write*
    (literal "/project/.env")
    (subpath "/project/config"))

$ ralon run -- claude
ralon: 4 paths locked via the seatbelt backend
The same shape as Linux — run, inherited across exec, impossible to leave. The difference is that Seatbelt can say deny, so the profile is the policy as you wrote it: a protected directory covers files created in it later, and nothing outside the named paths changes behaviour.

ralon install works here too, and enforces with something weaker, which is worth saying plainly. A Seatbelt profile is inherited and so cannot be imposed on an agent nobody started; a supervisor uses chflags uchg instead, which refuses every ordinary write from every process and which an agent can undo by running chflags nouchg. It is a narrowing, not a sandbox, and it is not equivalent to run.
Windowsexclusive file handles, held by a supervisor
$ ralon install                # once, ever
scope      C:\Users\dev
registered a Task Scheduler logon task

No scope covers D:\ — an agent.lock there is not enforced.
If that is where you keep code:
  ralon scope add D:\Projects

$ ralon scope add D:\Projects
scope      D:\Projects
enforcing  3 projects

$ cd D:\Projects\my-app
$ notepad agent.lock       # …that is the entire remaining step
No ralon run in sight, and nothing per repository. Because the locks are held rather than inherited, one background process covers agents Ralon never started — from an IDE, an extension, another terminal, or installed next month. Where Ralon is installed does not decide what it protects: a home directory on C: says nothing about a repository on D:, so install names the drives no scope reaches instead of guessing.

A guard refuses writes to the paths you declared and nothing else, so your build, tests, dev server, editor and git carry on normally. It cannot refuse only an LLM agent — a process carries no mark saying what it is, and agents write through cmd, python, node and git, the same binaries you use. The only person it gets in the way of is you — agent.lock protects itself, so you cannot rewrite your own policy while it is enforced, which is what ralon pause is for. A pause expires on its own by default: one that is forgotten about is a project that stopped being protected without anyone deciding it should.

[04]

How it holds

Inherited, one-way

On Linux and macOS the restriction survives fork and exec, and there is no syscall to leave it. Every descendant is born inside it, including the ones that outlive the agent.

Held, machine-wide

On Windows a handle is refused to every process, not just the ones Ralon started. An ACL would not do: the agent runs as the same user, so any permission Ralon can set it can unset.

Nothing to bypass

No daemon, no approval workflow, no file descriptor handed to the child. Under run, Ralon becomes your command.

Honest when it can't

If no backend is available, run refuses to start the command rather than running it unprotected — and says what to do instead.

Four backends, picked automatically per platform. Read how they differ — the choice is visible, and two of them have a cost worth knowing about.

[05]

Wire it into an agent

ralon init already did this; ralon hook install does it on its own, and --no-hooks skips it. It writes a refusal into the configuration of every agent that documents a hook capable of blocking an edit before it happens — nine of them.

AgentFileHow it refuses
Claude Code.claude/settings.jsonpermissionDecision: deny
GitHub Copilot.github/hooks/ralon.jsonpermissionDecision: deny
OpenAI Codex.codex/hooks.jsonpermissionDecision: deny, or exit 2
Cursor.cursor/hooks.jsonpermission: deny
Gemini CLI.gemini/settings.jsondecision: deny
Google Antigravity.agents/hooks.jsondecision: deny
Cline.clinerules/hooks/PreToolUsecancel: true
Windsurf / Cascade.windsurf/hooks.jsonexit 2
OpenCode.opencode/plugins/ralon.jsthrows

One ralon hook check serves all nine: the refusal is a single JSON document carrying every one of those keys, plus exit code 2. Emitting a key an agent ignores costs nothing; omitting one it needs is an edit waved through. Reads are never refused — some agents call the hook for every tool, and an agent should be able to read the policy governing it.

A courtesy, not a defence. It covers the agent's edit tools and not the shell commands it runs, because a hook cannot tell which paths sed -i will touch. Enforcement does not care: it restricts the process, which is why run and guard already cover Aider, Amazon Q, Junie, Roo Code and whatever ships next year. Agents are listed above only because a hook has to speak each one's config format.