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.
version: 1 protect: - src/index.tsx - src/auth.ts - .env - config/**
- agent.locklocked
- .envlocked
- src/
- index.tsxlocked
- auth.tslocked
- App.tsxwritable
- utils.tswritable
- config/locked
- tests/writable
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.
$ echo hacked > src/index.tsx sh: src/index.tsx: Read-only file system # exit 1 — the file on disk is untouched
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.
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.
$ 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 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
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.$ 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
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.
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.
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.
| Agent | File | How it refuses |
|---|---|---|
| Claude Code | .claude/settings.json | permissionDecision: deny |
| GitHub Copilot | .github/hooks/ralon.json | permissionDecision: deny |
| OpenAI Codex | .codex/hooks.json | permissionDecision: deny, or exit 2 |
| Cursor | .cursor/hooks.json | permission: deny |
| Gemini CLI | .gemini/settings.json | decision: deny |
| Google Antigravity | .agents/hooks.json | decision: deny |
| Cline | .clinerules/hooks/PreToolUse | cancel: true |
| Windsurf / Cascade | .windsurf/hooks.json | exit 2 |
| OpenCode | .opencode/plugins/ralon.js | throws |
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.