1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use crate::enforce::Backend;
/// Filesystem policy for AI coding agents.
///
/// `agent.lock` declares what AI-controlled processes may not modify, the same
/// way `.gitignore` declares what Git may not track.
#[derive(Debug, Parser)]
#[command(name = "ralon", version, about, long_about = None)]
pub struct Cli {
/// Directory to look for agent.lock in (default: the current directory)
#[arg(short = 'C', long = "dir", global = true, value_name = "DIR")]
pub directory: Option<PathBuf>,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Set this machine up once. After that: declare a policy, and enforcement
/// starts on its own
///
/// Install once → declare policy → enforcement starts automatically. There
/// is no third step and nothing to run inside a project: writing an
/// `agent.lock` is what turns enforcement on, and deleting it is what turns
/// enforcement off.
///
/// Registers a background supervisor with the operating system — a logon
/// task on Windows, a LaunchAgent on macOS — and records which directories
/// your projects live in.
///
/// Not available on Linux, where a restriction can only be inherited by a
/// process at startup and never imposed on one already running. `ralon
/// install` explains that rather than registering a service that could
/// notice a policy and do nothing about it.
Install {
/// A directory your projects live in. Repeatable.
/// Defaults to your home directory.
#[arg(long = "scope", value_name = "DIR")]
scope: Vec<PathBuf>,
/// How deep inside a scope a project may be
#[arg(long, value_name = "N")]
depth: Option<usize>,
/// Do not configure the agents
///
/// By default each enforced project gets the agent hook, so an agent is
/// told "protected by Ralon" instead of being handed the raw OS error.
/// Without it the policy is still enforced — the agent just reports
/// `EBUSY: resource busy or locked` and has to work out why.
#[arg(long)]
no_hooks: bool,
/// Print what would be registered and change nothing
#[arg(long)]
dry_run: bool,
},
/// Manage the directories your projects live in
///
/// A scope is where Ralon will honour an `agent.lock`. Repositories under one
/// need no configuration of their own; repositories outside every scope are
/// not enforced, and `ralon status` says so rather than looking protected.
///
/// Where Ralon is installed has nothing to do with this. If your code is on
/// another drive, say so:
///
/// ralon scope add D:\Projects
Scope {
#[command(subcommand)]
action: ScopeAction,
},
/// Remove the background supervisor and release everything it holds
Uninstall {
/// Deregister the supervisor but leave the current enforcement in place
#[arg(long)]
keep_enforcement: bool,
},
/// Release one project so its policy can be edited, then take it back
///
/// `agent.lock` protects itself, so a project the supervisor is enforcing
/// cannot have its own policy rewritten — including by you. This is how you
/// get it back for a while.
Pause {
/// Minutes before enforcement resumes on its own
#[arg(long, value_name = "MINUTES", default_value_t = 15)]
minutes: u64,
/// Stay paused until `ralon resume`. You have to ask for this: a pause
/// that is forgotten about is a project that stopped being protected
/// without anyone deciding it should.
#[arg(long, conflicts_with = "minutes")]
indefinitely: bool,
},
/// Resume enforcement for a paused project
Resume,
/// The background supervisor itself. Started by the OS, not by people.
Daemon {
/// Stay in this process. What launchd and Task Scheduler want.
#[arg(long)]
foreground: bool,
/// Do one pass and exit, printing what changed
#[arg(long, conflicts_with = "foreground")]
once: bool,
/// Where to keep supervisor state, passed by the registered service
#[arg(long, value_name = "DIR")]
home: Option<PathBuf>,
},
/// Write a starter agent.lock and wire up the agents
Init {
/// Overwrite an existing agent.lock
#[arg(long)]
force: bool,
/// Write the policy only, and configure nothing
#[arg(long)]
no_hooks: bool,
},
/// Report what the policy protects, or whether given paths are protected
///
/// Exits 1 if any given path is protected, which makes it usable as an
/// agent pre-write hook.
Check {
/// Paths to test. With none, lists everything the policy protects.
#[arg(value_name = "PATH")]
paths: Vec<PathBuf>,
},
/// Show the policy and which enforcement backends this kernel offers
Status,
/// Install or run the agent hook
///
/// The hook refuses an agent's own edit tools before they touch a protected
/// path. It is a courtesy layer, not enforcement — an agent that shells out
/// bypasses it — but on platforms `run` cannot restrict, it is the only
/// thing standing between an agent and your policy.
Hook {
#[command(subcommand)]
action: HookAction,
},
/// Protect the project against every process, with no command to wrap
///
/// `run` protects the agent it starts. A guard protects the ones it does
/// not: it holds the locks itself, and Windows refuses those to every
/// process on the machine, so an agent started from anywhere — a terminal,
/// an IDE, an extension, one installed next month — is refused without
/// knowing Ralon exists. Start it once and stop it with `--stop`.
Guard {
/// Keep running after this terminal closes
#[arg(long, conflicts_with = "stop")]
detach: bool,
/// Release a running guard and clear anything it left behind
#[arg(long)]
stop: bool,
/// This *is* the background guard `--detach` started. Not for people:
/// it means "you have no console, do not try to write to one".
#[arg(long, hide = true, conflicts_with_all = ["detach", "stop"])]
detached: bool,
},
/// Run a command that cannot modify the protected paths
Run {
/// Enforcement backend
#[arg(long, value_enum, default_value_t = Backend::Auto)]
backend: Backend,
/// Print what would be enforced and exit
#[arg(long)]
dry_run: bool,
/// Do not print the lock summary before running
#[arg(short, long)]
quiet: bool,
/// Command to run, e.g. `ralon run -- claude`
#[arg(
value_name = "COMMAND",
required = true,
trailing_var_arg = true,
allow_hyphen_values = true
)]
command: Vec<OsString>,
},
}
#[derive(Debug, Subcommand)]
pub enum ScopeAction {
/// Start honouring `agent.lock` under a directory
///
/// Takes effect immediately: any project already there with a policy is
/// enforced before this command returns.
Add {
/// Directories to cover. Repeatable.
#[arg(value_name = "DIR", required = true)]
directories: Vec<PathBuf>,
},
/// Show the scopes, and what is enforced in each
List,
/// Stop honouring `agent.lock` under a directory
///
/// Releases every project it was enforcing there, before returning. Must
/// name a scope exactly — a directory *inside* one cannot be carved out.
Remove {
/// Scopes to drop. Repeatable.
#[arg(value_name = "DIR", required = true)]
directories: Vec<PathBuf>,
},
}
#[derive(Debug, Subcommand)]
pub enum HookAction {
/// Wire the hook into an agent's configuration
Install {
/// Which agent to configure
#[arg(long, value_enum, default_value_t = Agent::All)]
agent: Agent,
/// Print the configuration instead of writing it
#[arg(long)]
dry_run: bool,
},
/// Decide one edit, reading the agent's request on stdin
///
/// This is what the installed hook calls. Parsing the request here rather
/// than in a shell snippet keeps the configuration free of quoting, and
/// means the hook behaves identically on every platform.
Check,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum Agent {
/// Every agent below. A policy should hold whichever tool the project is
/// opened with, and you cannot know that in advance.
All,
/// Claude Code — .claude/settings.json
Claude,
/// Cursor — .cursor/hooks.json
Cursor,
/// OpenCode — .opencode/plugins/ralon.js
Opencode,
/// GitHub Copilot in VS Code — .github/hooks/ralon.json
Copilot,
/// OpenAI Codex — .codex/hooks.json
Codex,
/// Gemini CLI — .gemini/settings.json
Gemini,
/// Google Antigravity — .agents/hooks.json
Antigravity,
/// Windsurf / Cascade — .windsurf/hooks.json
Windsurf,
/// Cline — .clinerules/hooks/PreToolUse
Cline,
}