Skip to main content

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    /// Check the command as a hook does when the tool does not say which folder it runs in, with
53    /// writes approved as far as LEVEL allows: reads, developer or workspace. --root is the
54    /// workspace; --cwd is ignored.
55    #[arg(long, value_name = "LEVEL", value_parser = ["reads", "developer", "workspace"])]
56    pub unknown_folder: Option<String>,
57
58    /// The harness session id (as a hook would pass), used to recognize this session's scratchpad
59    /// under a temp root as a trusted working area rather than anonymous `/tmp`.
60    #[arg(long, value_name = "ID")]
61    pub session_id: Option<String>,
62
63    /// Print a per-segment breakdown of why a command would or would not auto-approve. For a
64    /// single command it also prints the behavior it resolved to and, when it does not approve,
65    /// names the facet that refused it.
66    #[arg(long)]
67    pub explain: bool,
68
69    /// Show how to support a command safe-chains doesn't recognize yet. Pass the command as the
70    /// argument: `safe-chains --suggest "<command>"`. It prints the `.safe-chains.toml` entry for
71    /// the unrecognized command, the path that file belongs at, and the `[[trusted]]` pin to add to
72    /// ~/.config/safe-chains.toml so it takes effect. Writes nothing — copy the parts you want.
73    #[arg(long)]
74    pub suggest: bool,
75
76    /// Record every command that did NOT auto-approve — with the directory it ran in and why it was
77    /// refused — as JSON Lines in ~/.local/state/safe-chains/log.jsonl. Off unless given. The file
78    /// holds commands verbatim, credentials included, and is created owner-only.
79    #[arg(long)]
80    pub log: bool,
81
82    /// Like --log, but records approvals too. Writes an entry per command rather than per refusal.
83    #[arg(long)]
84    pub log_everything: bool,
85
86    /// List all supported commands in Markdown format
87    #[arg(long)]
88    pub list_commands: bool,
89
90    /// Generate mdBook command reference pages in docs/src/commands/
91    #[arg(long)]
92    pub generate_book: bool,
93
94    /// Configure the hook for the named tool (default: claude). Use --auto-detect for every installed tool.
95    #[arg(long)]
96    pub setup: bool,
97
98    /// Pair with --setup to select the target tool by name. See --list-tools.
99    #[arg(long, value_name = "NAME")]
100    pub tool: Option<String>,
101
102    /// Pair with --setup to install for every installed tool detected on this machine.
103    #[arg(long)]
104    pub auto_detect: bool,
105
106    /// Print the names of every supported integration target.
107    #[arg(long)]
108    pub list_tools: bool,
109
110    /// Hook subcommand: read this tool's stdin envelope, validate the command, write the response.
111    #[command(subcommand)]
112    pub subcommand: Option<Subcommand>,
113}
114
115#[derive(clap::Subcommand)]
116pub enum Subcommand {
117    /// Run as a runtime hook for the named tool.
118    Hook {
119        /// Tool to read/write the hook envelope for. See --list-tools.
120        #[arg(value_name = "TOOL")]
121        tool: String,
122
123        /// How far to approve writes when this tool does not say which folder the command runs
124        /// in: reads, developer or workspace. Overrides `[unknown_folder] writes` in
125        /// ~/.config/safe-chains.toml. Default: developer.
126        #[arg(long, value_name = "LEVEL", value_parser = ["reads", "developer", "workspace"])]
127        unknown_folder: Option<String>,
128    },
129}