Skip to main content

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}