safe_chains/cli.rs
1use clap::Parser;
2
3const EXAMPLES: &str = "\
4EXAMPLES:
5 # Check whether a command would auto-approve (exit 0 = allowed):
6 safe-chains \"git status\"
7
8 # See a per-segment breakdown of why a command does or doesn't approve:
9 safe-chains --explain \"grep foo . && ./deploy.sh\"
10
11 # For a single command, --explain also shows the behavior it resolved to and
12 # names the facet that refused it (e.g. locus.remote = fixed):
13 safe-chains --explain \"aws dynamodb put-item --table-name t --item {}\"
14
15 # Offer to support a command safe-chains doesn't recognize. This writes or
16 # upgrades the local .safe-chains.toml, then prints the pin to hand-add to
17 # ~/.config/safe-chains.toml so the definition takes effect:
18 safe-chains --suggest \"mytool sync --dry-run\"
19";
20
21#[derive(Parser)]
22#[command(name = "safe-chains")]
23#[command(about = "Auto-allow safe bash commands in agentic coding tools")]
24#[command(version, disable_version_flag = true)]
25#[command(after_help = EXAMPLES)]
26#[allow(clippy::struct_excessive_bools)]
27pub struct Cli {
28 /// Command string to check (omit for Claude hook mode via stdin)
29 pub command: Option<String>,
30
31 /// Print version information.
32 #[arg(short = 'v', short_alias = 'V', long, action = clap::ArgAction::Version)]
33 pub version: Option<bool>,
34
35 /// Safety level threshold; only commands at or below it auto-approve. Levels, locked → open:
36 /// paranoid, reader, editor, developer, local-admin, network-admin, yolo. The legacy names
37 /// inert / safe-read / safe-write still work (mapped to paranoid / reader / developer, with a
38 /// notice). Default: developer.
39 #[arg(long)]
40 pub level: Option<String>,
41
42 /// Working directory to resolve relative paths against (as a harness hook would pass).
43 /// Pair with --root so e.g. `cd`-relative writes classify against the real directory.
44 #[arg(long)]
45 pub cwd: Option<String>,
46
47 /// Project root, so a relative path under it is worktree-local and one outside it (the
48 /// cwd having escaped the project) is scored as its real absolute target.
49 #[arg(long)]
50 pub root: Option<String>,
51
52 /// The harness session id (as a hook would pass), used to recognize this session's scratchpad
53 /// under a temp root as a trusted working area rather than anonymous `/tmp`.
54 #[arg(long, value_name = "ID")]
55 pub session_id: Option<String>,
56
57 /// Print a per-segment breakdown of why a command would or would not auto-approve. For a
58 /// single command it also prints the behavior it resolved to and, when it does not approve,
59 /// names the facet that refused it.
60 #[arg(long)]
61 pub explain: bool,
62
63 /// Offer to support a command safe-chains doesn't recognize yet. Pass the command as the
64 /// argument: `safe-chains --suggest "<command>"`. It writes (or upgrades) the project's local
65 /// `.safe-chains.toml` with a definition for the unrecognized command, then prints the
66 /// `[[trusted]]` pin for you to add to ~/.config/safe-chains.toml so that file takes effect.
67 #[arg(long)]
68 pub suggest: bool,
69
70 /// Record every command that did NOT auto-approve — with the directory it ran in and why it was
71 /// refused — as JSON Lines in ~/.local/state/safe-chains/log.jsonl. Off unless given. The file
72 /// holds commands verbatim, credentials included, and is created owner-only.
73 #[arg(long)]
74 pub log: bool,
75
76 /// Like --log, but records approvals too. Writes an entry per command rather than per refusal.
77 #[arg(long)]
78 pub log_everything: bool,
79
80 /// List all supported commands in Markdown format
81 #[arg(long)]
82 pub list_commands: bool,
83
84 /// Generate mdBook command reference pages in docs/src/commands/
85 #[arg(long)]
86 pub generate_book: bool,
87
88 /// Configure the hook for the named tool (default: claude). Use --auto-detect for every installed tool.
89 #[arg(long)]
90 pub setup: bool,
91
92 /// Pair with --setup to select the target tool by name. See --list-tools.
93 #[arg(long, value_name = "NAME")]
94 pub tool: Option<String>,
95
96 /// Pair with --setup to install for every installed tool detected on this machine.
97 #[arg(long)]
98 pub auto_detect: bool,
99
100 /// Print the names of every supported integration target.
101 #[arg(long)]
102 pub list_tools: bool,
103
104 /// Hook subcommand: read this tool's stdin envelope, validate the command, write the response.
105 #[command(subcommand)]
106 pub subcommand: Option<Subcommand>,
107}
108
109#[derive(clap::Subcommand)]
110pub enum Subcommand {
111 /// Run as a runtime hook for the named tool.
112 Hook {
113 /// Tool to read/write the hook envelope for. See --list-tools.
114 #[arg(value_name = "TOOL")]
115 tool: String,
116 },
117}