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 /// Wire `murk mcp` into an AI editor's MCP config, minting a scoped grant.
512 /// No CLIENT auto-detects installed editors; give one (claude, cursor,
513 /// vscode, zed, gemini, omp, codex) to target it.
514 Connect {
515 /// Editor to configure; omit to auto-detect
516 /// (claude, cursor, vscode, zed, gemini, omp, codex)
517 client: Option<String>,
518 /// Keys the agent may read (required — fails closed)
519 #[arg(long, required = true)]
520 only: Vec<String>,
521 /// Set the agent allow-list to these tags before granting (repeatable)
522 #[arg(long = "allow-tag")]
523 allow_tag: Vec<String>,
524 /// Also expose `murk agent exec` to the agent (adds --allow-exec)
525 #[arg(long)]
526 allow_exec: bool,
527 /// Grant time to live, e.g. 30m, 2h, 7d (advisory — see `agent revoke`)
528 #[arg(long, default_value = "2h")]
529 ttl: String,
530 /// Grant name (used to disconnect/revoke it later)
531 #[arg(long, default_value = "mcp")]
532 name: String,
533 /// Vault filename
534 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
535 vault: String,
536 },
537
538 /// Remove murk's entry from an AI editor's MCP config. No CLIENT clears every
539 /// configured editor; `--rotate` also revokes the grant and rotates its keys.
540 Disconnect {
541 /// Editor to disconnect; omit for every configured editor
542 client: Option<String>,
543 /// Revoke the grant and rotate the keys it could read
544 #[arg(long)]
545 rotate: bool,
546 /// Grant name to revoke with --rotate
547 #[arg(long, default_value = "mcp")]
548 name: String,
549 /// Vault filename
550 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
551 vault: String,
552 },
553}
554
555#[derive(Subcommand)]
556pub enum CircleCommand {
557 /// Add a recipient to the vault
558 Authorize {
559 /// Public key (age1...), ssh:path, ssh: (default ~/.ssh/id_ed25519.pub), or github:username
560 pubkey: String,
561 /// Display name for this recipient
562 #[arg(long)]
563 name: Option<String>,
564 /// Also add the new recipient to this group
565 #[arg(long)]
566 group: Option<String>,
567 /// Accept changed GitHub keys without confirmation
568 #[arg(long)]
569 force: bool,
570 /// Allow ssh-rsa recipients (rejected by default — use ed25519)
571 #[arg(long)]
572 allow_ssh_rsa: bool,
573 /// Vault filename
574 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
575 vault: String,
576 },
577
578 /// Remove a recipient from the vault
579 Revoke {
580 /// Recipient pubkey or display name
581 recipient: String,
582 /// Rotate the secrets they had access to in the same session
583 #[arg(long)]
584 rotate: bool,
585 /// Vault filename
586 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
587 vault: String,
588 },
589}
590
591#[derive(Subcommand)]
592pub enum GroupCommand {
593 /// Create a new recipient group (you become its first member)
594 Create {
595 /// Group name
596 name: String,
597 /// Vault filename
598 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
599 vault: String,
600 },
601
602 /// List groups and their members
603 Ls {
604 /// Output as JSON
605 #[arg(long)]
606 json: bool,
607 /// Vault filename
608 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
609 vault: String,
610 },
611
612 /// Add a member to a group
613 Add {
614 /// Group name
615 name: String,
616 /// Recipient pubkey or display name to add
617 #[arg(long)]
618 member: String,
619 /// Vault filename
620 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
621 vault: String,
622 },
623
624 /// Remove a member from a group, or delete the group entirely
625 Rm {
626 /// Group name
627 name: String,
628 /// Recipient pubkey or display name to remove (omit to delete the group)
629 #[arg(long)]
630 member: Option<String>,
631 /// Vault filename
632 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
633 vault: String,
634 },
635}
636
637#[derive(Subcommand)]
638pub enum PolicyCommand {
639 /// Show the agent access policy (works without a key)
640 Show {
641 /// Output as JSON
642 #[arg(long)]
643 json: bool,
644 /// Vault filename
645 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
646 vault: String,
647 },
648
649 /// Set the agent allow-list: agents may only receive secrets carrying one of these tags
650 Set {
651 /// Tag agents are allowed to receive (repeatable, required)
652 #[arg(long = "allow-tag", required = true)]
653 allow_tag: Vec<String>,
654 /// Vault filename
655 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
656 vault: String,
657 },
658
659 /// Remove the policy — agent mode becomes unrestricted again
660 Clear {
661 /// Vault filename
662 #[arg(long, env = "MURK_VAULT", default_value = ".murk")]
663 vault: String,
664 },
665}