murk_cli/cli.rs
1//! CLI command model: the clap `Parser`/`Subcommand` types.
2//!
3//! Defined in the library (not `main.rs`) so both the `murk` binary and the
4//! `doc-gen` tool build the exact same command tree from `Cli::command()`.
5
6use clap::{Parser, Subcommand};
7
8/// Encrypted secrets manager for developers.
9#[derive(Parser)]
10#[command(name = "murk", version, about)]
11pub struct Cli {
12 #[command(subcommand)]
13 pub command: Command,
14}
15
16#[derive(Subcommand)]
17pub enum Command {
18 // Setup & recovery
19 /// Initialize a new vault and generate a keypair
20 Init {
21 /// Vault filename
22 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
23 vault: String,
24 },
25
26 /// Write a .envrc for direnv integration
27 Env {
28 /// Vault filename
29 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
30 vault: String,
31 },
32
33 /// Restore MURK_KEY from a BIP39 recovery phrase
34 Restore {
35 /// Vault filename, for the restored-identity recipient check
36 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
37 vault: String,
38 },
39
40 /// Re-derive recovery phrase from current MURK_KEY
41 Recover,
42
43 // Secrets
44 /// Add or update a secret
45 Add {
46 /// Secret key name
47 key: String,
48 /// Description for this key
49 #[arg(long)]
50 desc: Option<String>,
51 /// Who can read it: a group name, `everyone` (default), or `me`
52 #[arg(long)]
53 group: Option<String>,
54 /// Deprecated alias for `--group me`
55 #[arg(long, hide = true)]
56 scoped: bool,
57 /// Tag for grouping (repeatable)
58 #[arg(long)]
59 tag: Vec<String>,
60 /// Vault filename
61 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
62 vault: String,
63 },
64
65 /// Generate a random secret and store it
66 Generate {
67 /// Secret key name
68 key: String,
69 /// Length in bytes (default 32)
70 #[arg(long, default_value = "32")]
71 length: usize,
72 /// Output as hex instead of base64
73 #[arg(long)]
74 hex: bool,
75 /// Description for this key
76 #[arg(long)]
77 desc: Option<String>,
78 /// Who can read it: a group name, `everyone` (default), or `me`
79 #[arg(long)]
80 group: Option<String>,
81 /// Tag for grouping (repeatable)
82 #[arg(long)]
83 tag: Vec<String>,
84 /// Vault filename
85 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
86 vault: String,
87 },
88
89 /// Rotate secrets with new values
90 Rotate {
91 /// Secret key name (omit for --all)
92 key: Option<String>,
93 /// Rotate all secrets in the vault
94 #[arg(long)]
95 all: bool,
96 /// Generate random values instead of prompting
97 #[arg(long)]
98 generate: bool,
99 /// Length in bytes for generated values (default 32)
100 #[arg(long, default_value = "32")]
101 length: usize,
102 /// Output generated values as hex instead of base64
103 #[arg(long)]
104 hex: bool,
105 /// List keys needing rotation instead of rotating (exits 1 if any)
106 #[arg(long, conflicts_with_all = ["key", "all", "generate", "hex"])]
107 list: bool,
108 /// Output the listing as JSON (with --list; always exits 0)
109 #[arg(long, requires = "list", conflicts_with_all = ["key", "all", "generate", "hex"])]
110 json: bool,
111 /// Vault filename
112 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
113 vault: String,
114 },
115
116 /// Remove a secret
117 Rm {
118 /// Secret key name
119 key: String,
120 /// Vault filename
121 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
122 vault: String,
123 },
124
125 /// Get a single decrypted value
126 Get {
127 /// Secret key name
128 key: String,
129 /// Vault filename
130 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
131 vault: String,
132 },
133
134 /// Edit secrets in $EDITOR
135 Edit {
136 /// Edit a single key (omit to edit all)
137 key: Option<String>,
138 /// Edit scoped overrides instead of shared secrets
139 #[arg(long)]
140 scoped: bool,
141 /// Edit values for this group instead of shared secrets
142 #[arg(long)]
143 group: Option<String>,
144 /// Vault filename
145 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
146 vault: String,
147 },
148
149 /// List all key names
150 Ls {
151 /// Filter by tag (repeatable)
152 #[arg(long)]
153 tag: Vec<String>,
154 /// Output as JSON
155 #[arg(long)]
156 json: bool,
157 /// Vault filename
158 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
159 vault: String,
160 },
161
162 /// Export all secrets as shell export statements
163 Export {
164 /// Filter by tag (repeatable)
165 #[arg(long)]
166 tag: Vec<String>,
167 /// Output as JSON
168 #[arg(long)]
169 json: bool,
170 /// Vault filename
171 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
172 vault: String,
173 },
174
175 /// Import secrets from a .env file
176 Import {
177 /// Path to the .env file to import
178 #[arg(default_value = ".env")]
179 file: String,
180 /// Overwrite existing secrets without prompting
181 #[arg(long)]
182 force: bool,
183 /// Assign imported secrets to this group (default: everyone)
184 #[arg(long)]
185 group: Option<String>,
186 /// Vault filename
187 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
188 vault: String,
189 },
190
191 // Metadata & inspection
192 /// Add or update a key description
193 Describe {
194 /// Secret key name
195 key: String,
196 /// Description text
197 description: String,
198 /// Example value
199 #[arg(long)]
200 example: Option<String>,
201 /// Tag for grouping (repeatable, replaces existing tags)
202 #[arg(long)]
203 tag: Vec<String>,
204 /// Rotation interval, e.g. `90d` or `90` (days); `never` clears it
205 #[arg(long, value_name = "DAYS")]
206 rotate_every: Option<String>,
207 /// Hard expiry date, e.g. `2026-09-01`; `never` clears it
208 #[arg(long, value_name = "DATE")]
209 expires: Option<String>,
210 /// Vault filename
211 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
212 vault: String,
213 },
214
215 /// Show public schema and key info
216 Info {
217 /// Filter by tag (repeatable)
218 #[arg(long)]
219 tag: Vec<String>,
220 /// Output as JSON
221 #[arg(long)]
222 json: bool,
223 /// Vault filename
224 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
225 vault: String,
226 },
227
228 /// Export schema-only vault with no secrets or recipients
229 Skeleton {
230 /// Output file (prints to stdout if omitted)
231 #[arg(long, short)]
232 output: Option<String>,
233 /// Vault filename
234 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
235 vault: String,
236 },
237
238 // Run
239 /// Run a command with secrets injected as environment variables
240 #[command(trailing_var_arg = true)]
241 Exec {
242 /// Only inject these specific keys (repeatable)
243 #[arg(long)]
244 only: Vec<String>,
245 /// Filter by tag (repeatable)
246 #[arg(long)]
247 tag: Vec<String>,
248 /// Strip inherited environment (only murk secrets + PATH)
249 #[arg(long)]
250 clean_env: bool,
251 /// Vault filename
252 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
253 vault: String,
254 /// Command and arguments to execute
255 #[arg(required = true)]
256 command: Vec<String>,
257 },
258
259 // Agents
260 /// Agent-oriented commands (schema-only output for AI agent prompts)
261 Agent {
262 #[command(subcommand)]
263 sub: AgentCommand,
264 },
265
266 /// Run an MCP (Model Context Protocol) stdio server for AI agents
267 Mcp {
268 /// Vault filename
269 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
270 vault: String,
271 /// Enable the murk_exec tool (run commands with scoped secrets injected).
272 /// Off by default: it runs arbitrary commands as this user — the injected
273 /// secrets are grant-scoped, but the command itself is not sandboxed.
274 #[arg(long = "allow-exec")]
275 allow_exec: bool,
276 },
277
278 /// Manage the agent access policy
279 Policy {
280 #[command(subcommand)]
281 sub: PolicyCommand,
282 },
283
284 // Recipients & groups
285 /// Manage recipients
286 #[command(alias = "recipients")]
287 Circle {
288 #[command(subcommand)]
289 sub: Option<CircleCommand>,
290 /// Output as JSON
291 #[arg(long)]
292 json: bool,
293 /// Vault filename
294 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
295 vault: String,
296 },
297
298 /// Add a recipient to the vault
299 #[command(hide = true)]
300 Authorize {
301 /// Public key (age1...), ssh:path, ssh: (default ~/.ssh/id_ed25519.pub), or github:username
302 pubkey: String,
303 /// Display name for this recipient
304 #[arg(long)]
305 name: Option<String>,
306 /// Accept changed GitHub keys without confirmation
307 #[arg(long)]
308 force: bool,
309 /// Allow ssh-rsa recipients (rejected by default — use ed25519)
310 #[arg(long)]
311 allow_ssh_rsa: bool,
312 /// Vault filename
313 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
314 vault: String,
315 },
316
317 /// Remove a recipient from the vault
318 #[command(hide = true)]
319 Revoke {
320 /// Recipient pubkey or display name
321 recipient: String,
322 /// Rotate the secrets they had access to in the same session
323 #[arg(long)]
324 rotate: bool,
325 /// Vault filename
326 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
327 vault: String,
328 },
329
330 /// Manage recipient groups
331 Group {
332 #[command(subcommand)]
333 sub: GroupCommand,
334 },
335
336 // Checks
337 /// Verify vault integrity without exporting secrets
338 Verify {
339 /// Vault filename
340 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
341 vault: String,
342 },
343
344 /// Check the surrounding repo for hygiene issues
345 Doctor {
346 /// Vault filename
347 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
348 vault: String,
349 },
350
351 /// Scan files for leaked secret values
352 Scan {
353 /// Files or directories to scan (defaults to current directory)
354 paths: Vec<String>,
355 /// Vault filename
356 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
357 vault: String,
358 },
359
360 // Git integration
361 /// Show secret changes vs a git ref
362 Diff {
363 /// Git ref to compare against
364 #[arg(default_value = "HEAD")]
365 git_ref: String,
366 /// Show actual values (not just key names)
367 #[arg(long)]
368 show_values: bool,
369 /// Output as JSON
370 #[arg(long)]
371 json: bool,
372 /// Vault filename
373 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
374 vault: String,
375 },
376
377 /// Configure git to use murk's merge driver for .murk files
378 #[command(name = "setup-merge-driver")]
379 SetupMergeDriver,
380
381 /// Git merge driver for .murk vault files (called by git)
382 #[command(name = "merge-driver", hide = true)]
383 MergeDriver {
384 /// Path to base version (%O)
385 base: String,
386 /// Path to ours version (%A) — result is written here
387 ours: String,
388 /// Path to theirs version (%B)
389 theirs: String,
390 },
391
392 // Shell
393 /// Generate or install shell completions
394 Completion {
395 #[command(subcommand)]
396 action: CompletionAction,
397 },
398}
399
400#[derive(Subcommand)]
401pub enum CompletionAction {
402 /// Print completions to stdout
403 Generate {
404 /// Shell to generate completions for
405 shell: clap_complete::Shell,
406 },
407 /// Install completions to the standard path
408 Install {
409 /// Shell to install completions for
410 shell: clap_complete::Shell,
411 },
412}
413
414#[derive(Subcommand)]
415pub enum AgentCommand {
416 /// Emit schema-only context safe to paste into an AI agent prompt
417 Plan {
418 /// Filter by tag (repeatable)
419 #[arg(long)]
420 tag: Vec<String>,
421 /// Output as JSON
422 #[arg(long)]
423 json: bool,
424 /// Output file (prints to stdout if omitted)
425 #[arg(long, short)]
426 output: Option<String>,
427 /// Vault filename
428 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
429 vault: String,
430 },
431
432 /// Run a command with strict agent-safe defaults (clears the inherited
433 /// environment, strips MURK_KEY, requires --only)
434 #[command(trailing_var_arg = true)]
435 Exec {
436 /// Inject these specific keys (required — agent mode fails closed)
437 #[arg(long, required = true)]
438 only: Vec<String>,
439 /// Vault filename
440 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
441 vault: String,
442 /// Command and arguments to execute
443 #[arg(required = true)]
444 command: Vec<String>,
445 },
446
447 /// Mint a short-lived ephemeral key that can read only the named secrets
448 Grant {
449 /// Grant name (used to revoke it later)
450 #[arg(long)]
451 name: String,
452 /// Keys this grant can read (required — fails closed)
453 #[arg(long, required = true)]
454 only: Vec<String>,
455 /// Time to live, e.g. 30m, 2h, 7d (advisory — see `agent revoke`)
456 #[arg(long, default_value = "2h")]
457 ttl: String,
458 /// Where to write the agent key: a path, or `-` for stdout
459 #[arg(long)]
460 out: Option<String>,
461 /// Vault filename
462 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
463 vault: String,
464 },
465
466 /// One-shot onboarding: optionally set the agent allow-list, mint a scoped
467 /// grant, and print how to run the agent safely
468 Init {
469 /// Grant name (used to revoke it later)
470 #[arg(long)]
471 name: String,
472 /// Keys the agent can read (required — fails closed)
473 #[arg(long, required = true)]
474 only: Vec<String>,
475 /// Set the agent allow-list to these tags before granting (repeatable)
476 #[arg(long = "allow-tag")]
477 allow_tag: Vec<String>,
478 /// Time to live, e.g. 30m, 2h, 7d (advisory — see `agent revoke`)
479 #[arg(long, default_value = "2h")]
480 ttl: String,
481 /// Where to write the agent key: a path, or `-` for stdout
482 #[arg(long)]
483 out: Option<String>,
484 /// Vault filename
485 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
486 vault: String,
487 },
488
489 /// List active agent grants and their TTLs
490 Ls {
491 /// Output as JSON
492 #[arg(long)]
493 json: bool,
494 /// Vault filename
495 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
496 vault: String,
497 },
498
499 /// Revoke an agent grant and rotate the keys it could read
500 Revoke {
501 /// Grant name
502 name: String,
503 /// Rotate the keys it could read in the same session
504 #[arg(long)]
505 rotate: bool,
506 /// Vault filename
507 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
508 vault: String,
509 },
510}
511
512#[derive(Subcommand)]
513pub enum CircleCommand {
514 /// Add a recipient to the vault
515 Authorize {
516 /// Public key (age1...), ssh:path, ssh: (default ~/.ssh/id_ed25519.pub), or github:username
517 pubkey: String,
518 /// Display name for this recipient
519 #[arg(long)]
520 name: Option<String>,
521 /// Also add the new recipient to this group
522 #[arg(long)]
523 group: Option<String>,
524 /// Accept changed GitHub keys without confirmation
525 #[arg(long)]
526 force: bool,
527 /// Allow ssh-rsa recipients (rejected by default — use ed25519)
528 #[arg(long)]
529 allow_ssh_rsa: bool,
530 /// Vault filename
531 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
532 vault: String,
533 },
534
535 /// Remove a recipient from the vault
536 Revoke {
537 /// Recipient pubkey or display name
538 recipient: String,
539 /// Rotate the secrets they had access to in the same session
540 #[arg(long)]
541 rotate: bool,
542 /// Vault filename
543 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
544 vault: String,
545 },
546}
547
548#[derive(Subcommand)]
549pub enum GroupCommand {
550 /// Create a new recipient group (you become its first member)
551 Create {
552 /// Group name
553 name: String,
554 /// Vault filename
555 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
556 vault: String,
557 },
558
559 /// List groups and their members
560 Ls {
561 /// Output as JSON
562 #[arg(long)]
563 json: bool,
564 /// Vault filename
565 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
566 vault: String,
567 },
568
569 /// Add a member to a group
570 Add {
571 /// Group name
572 name: String,
573 /// Recipient pubkey or display name to add
574 #[arg(long)]
575 member: String,
576 /// Vault filename
577 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
578 vault: String,
579 },
580
581 /// Remove a member from a group, or delete the group entirely
582 Rm {
583 /// Group name
584 name: String,
585 /// Recipient pubkey or display name to remove (omit to delete the group)
586 #[arg(long)]
587 member: Option<String>,
588 /// Vault filename
589 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
590 vault: String,
591 },
592}
593
594#[derive(Subcommand)]
595pub enum PolicyCommand {
596 /// Show the agent access policy (works without a key)
597 Show {
598 /// Output as JSON
599 #[arg(long)]
600 json: bool,
601 /// Vault filename
602 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
603 vault: String,
604 },
605
606 /// Set the agent allow-list: agents may only receive secrets carrying one of these tags
607 Set {
608 /// Tag agents are allowed to receive (repeatable, required)
609 #[arg(long = "allow-tag", required = true)]
610 allow_tag: Vec<String>,
611 /// Vault filename
612 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
613 vault: String,
614 },
615
616 /// Remove the policy — agent mode becomes unrestricted again
617 Clear {
618 /// Vault filename
619 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
620 vault: String,
621 },
622}