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