use std::io::{self, IsTerminal, Read, Write};
use std::process;
use clap::{CommandFactory, Parser};
use safe_chains::cli::{Cli, Subcommand};
use safe_chains::targets::{self, HookFormat};
use safe_chains::verdict::{SafetyLevel, Verdict};
fn print_docs() {
let docs = safe_chains::docs::all_command_docs();
print!("{}", safe_chains::docs::render_markdown(&docs));
}
fn run_cli(
command: &str,
threshold: SafetyLevel,
engine_level: Option<&'static safe_chains::engine::level::Level>,
) {
let verdict = safe_chains::command_verdict_ceilinged(command, threshold, engine_level);
process::exit(i32::from(!verdict.is_allowed()));
}
fn run_explain(command: &str) -> ! {
let explanation = safe_chains::cst::explain(command);
print!("{}", explanation.render());
process::exit(i32::from(!explanation.is_allowed()));
}
fn run_setup(name: Option<String>, auto_detect: bool) -> ! {
let Some(home) = std::env::var_os("HOME") else {
eprintln!("Error: HOME environment variable not set");
process::exit(1);
};
let home = std::path::PathBuf::from(home);
if auto_detect {
let detected = targets::detect_installed(&home);
if detected.is_empty() {
eprintln!(
"No supported tools detected on this machine. Run with --list-tools to see candidates."
);
process::exit(1);
}
let mut any_failed = false;
for target in detected {
match target.install(&home) {
Ok(outcome) => println!("{}", outcome.message(target.display_name())),
Err(e) => {
eprintln!("{}: {e}", target.display_name());
any_failed = true;
}
}
}
process::exit(i32::from(any_failed));
}
let target_name = name.as_deref().unwrap_or("claude");
let Some(target) = targets::find(target_name) else {
eprintln!("Unknown tool: {target_name}. Run with --list-tools to see candidates.");
process::exit(1);
};
match target.install(&home) {
Ok(outcome) => {
println!("{}", outcome.message(target.display_name()));
process::exit(0);
}
Err(e) => {
eprintln!("{}: {e}", target.display_name());
process::exit(1);
}
}
}
fn run_list_tools() -> ! {
for target in targets::registry() {
println!("{}\t{}", target.name(), target.display_name());
}
process::exit(0);
}
fn run_hook_for(target_name: &str) -> ! {
let Some(target) = targets::find(target_name) else {
eprintln!("Unknown tool: {target_name}. Run with --list-tools to see candidates.");
process::exit(1);
};
let Some(format) = target.hook_format() else {
eprintln!(
"{}: this target does not use a runtime hook (config-only integration).",
target.display_name()
);
process::exit(1);
};
run_hook_format(format);
}
fn run_hook_format(format: &dyn HookFormat) -> ! {
let mut buf = String::new();
if io::stdin().read_to_string(&mut buf).is_err() {
process::exit(0);
}
let Ok(input) = format.parse_input(&buf) else {
process::exit(0);
};
let _ctx = safe_chains::pathctx::enter(safe_chains::pathctx::PathCtx {
cwd: input.cwd.clone(),
root: input.root.clone().or_else(|| input.cwd.clone()),
});
let (threshold, engine_level) = safe_chains::configured_hook_ceiling();
let verdict = safe_chains::command_verdict_ceilinged(&input.command, threshold, engine_level);
if verdict.is_allowed() {
let response = format.render_response(verdict);
let _ = io::stdout().write_all(response.stdout.as_bytes());
process::exit(response.exit_code);
}
let explanation = safe_chains::explain_with_coverage_at_level(&input.command, engine_level);
if let Verdict::Allowed(level) = explanation.overall
&& level <= threshold
{
let response = format.render_response(Verdict::Allowed(level));
let _ = io::stdout().write_all(response.stdout.as_bytes());
process::exit(response.exit_code);
}
const DOCS_URL: &str = "https://www.michaeldhopkins.com/docs/safe-chains/how-it-works.html";
let overreach = safe_chains::workspace_overreach(&input.command);
let overreach_why = overreach.as_ref().map(|(path, reason)| reason.message(path));
match format.gated_policy() {
safe_chains::targets::GatedPolicy::Deny => {
let reason = match &overreach_why {
Some(why) => format!(
"safe-chains blocked this: {why}. This harness has no interactive approval. {DOCS_URL}"
),
None => format!(
"safe-chains blocked this: it is not on the allowlist and this harness has no \
interactive approval. To allow it, add a custom command or a grant to \
~/.config/safe-chains.toml. {DOCS_URL}"
),
};
let response = format.render_deny(&reason);
let _ = io::stdout().write_all(response.stdout.as_bytes());
process::exit(response.exit_code);
}
safe_chains::targets::GatedPolicy::Ask => {
let reason = match &overreach_why {
Some(why) => format!(
"safe-chains did not auto-approve this — please confirm: {why}. {DOCS_URL}"
),
None => "safe-chains did not auto-approve this command — please confirm. (Add it to \
~/.config/safe-chains.toml so safe-chains stops flagging it.)"
.to_string(),
};
let response = format.render_ask(&reason);
let _ = io::stdout().write_all(response.stdout.as_bytes());
process::exit(response.exit_code);
}
safe_chains::targets::GatedPolicy::Defer => {}
}
if explanation.should_surface() {
let response = format.render_context(&explanation.render());
let _ = io::stdout().write_all(response.stdout.as_bytes());
process::exit(response.exit_code);
}
if let Some((path, reason)) = overreach {
let nudge = format!(
"safe-chains did not auto-approve this: {}. {DOCS_URL}",
reason.message(&path)
);
let response = format.render_context(&nudge);
let _ = io::stdout().write_all(response.stdout.as_bytes());
process::exit(response.exit_code);
}
process::exit(0);
}
fn main() {
let cli = Cli::try_parse();
match cli {
Ok(cli) => {
if let Some(Subcommand::Hook { tool }) = cli.subcommand {
run_hook_for(&tool);
}
if cli.list_tools {
run_list_tools();
}
if cli.setup {
run_setup(cli.tool, cli.auto_detect);
}
if cli.list_commands {
print_docs();
} else if cli.generate_book {
let docs = safe_chains::docs::all_command_docs();
safe_chains::docs::render_book(&docs, std::path::Path::new("docs"));
} else if let Some(command) = cli.command {
let _ctx = safe_chains::pathctx::enter(safe_chains::pathctx::PathCtx {
cwd: cli.cwd,
root: cli.root,
});
if cli.explain {
run_explain(&command);
}
let (threshold, engine_level) = match cli.level.as_deref() {
None => (SafetyLevel::SafeWrite, None), Some(name) => {
if let Some((_, Some(current))) = SafetyLevel::resolve_threshold(name) {
eprintln!(
"note: '--level {name}' is a legacy level name — mapping to \
'{current}'. Current levels: paranoid, reader, editor, \
developer, local-admin, network-admin, yolo."
);
}
match safe_chains::level_ceiling(name) {
Some(pair) => pair,
None => {
eprintln!(
"Error: unknown --level '{name}'. Levels: paranoid, reader, editor, \
developer, local-admin, network-admin, yolo (legacy: inert, \
safe-read, safe-write)."
);
process::exit(2);
}
}
}
};
run_cli(&command, threshold, engine_level);
} else if io::stdin().is_terminal() {
Cli::command().print_help().ok();
println!();
process::exit(2);
} else {
let claude = targets::find("claude").expect("claude target registered");
let format = claude
.hook_format()
.expect("claude target has a hook format");
run_hook_format(format);
}
}
Err(e) => e.exit(),
}
}