#![allow(clippy::unwrap_used)]
use std::io::Write;
use std::process::{Command, Stdio};
fn binary() -> &'static str {
env!("CARGO_BIN_EXE_safe-chains")
}
fn run_hook(args: &[&str], stdin_payload: &str) -> (String, String, i32) {
let mut child = Command::new(binary())
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn safe-chains");
match child.stdin.as_mut().unwrap().write_all(stdin_payload.as_bytes()) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {}
Err(e) => panic!("write to safe-chains stdin failed: {e}"),
}
let out = child.wait_with_output().expect("wait");
(
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
out.status.code().unwrap_or(-1),
)
}
fn hook_at_level(level: &str, command: &str) -> (String, i32) {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let home = std::env::temp_dir()
.join(format!("sc-lvl-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed)));
std::fs::create_dir_all(home.join(".config")).unwrap();
std::fs::write(home.join(".config/safe-chains.toml"), format!("level = \"{level}\"\n")).unwrap();
let payload =
format!(r#"{{"tool_input": {{"command": "{command}"}}, "cwd": "{}"}}"#, home.display());
let mut child = Command::new(binary())
.args(["hook", "claude"])
.env("HOME", &home)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn");
let _ = child.stdin.as_mut().unwrap().write_all(payload.as_bytes());
let out = child.wait_with_output().expect("wait");
let _ = std::fs::remove_dir_all(&home);
(String::from_utf8_lossy(&out.stdout).into_owned(), out.status.code().unwrap_or(-1))
}
fn decides_allow(stdout: &str) -> bool {
serde_json::from_str::<serde_json::Value>(stdout.trim())
.ok()
.and_then(|v| {
v.pointer("/hookSpecificOutput/permissionDecision")
.and_then(|d| d.as_str())
.map(|s| s == "allow")
})
.unwrap_or(false)
}
#[test]
fn configured_level_network_admin_raises_the_ceiling() {
let (stdout, code) = hook_at_level("network-admin", "git push origin main");
assert_eq!(code, 0);
assert!(decides_allow(&stdout), "network-admin approves git push: {stdout}");
}
#[test]
fn configured_level_reader_lowers_the_ceiling_and_gates_writes() {
assert!(decides_allow(&hook_at_level("reader", "cat ./notes.txt").0), "reader approves a read");
let (stdout, code) = hook_at_level("reader", "echo hi > ./notes.txt");
assert_eq!(code, 0);
assert!(!decides_allow(&stdout), "reader gates a worktree write: {stdout}");
}
#[test]
fn configured_level_editor_gates_destroys_but_not_writes() {
assert!(decides_allow(&hook_at_level("editor", "echo hi > ./notes.txt").0), "editor writes a worktree file");
let (stdout, code) = hook_at_level("editor", "rm ./notes.txt");
assert_eq!(code, 0);
assert!(!decides_allow(&stdout), "editor gates a worktree destroy: {stdout}");
assert!(decides_allow(&hook_at_level("developer", "rm ./notes.txt").0), "developer allows the destroy");
}
#[test]
fn configured_level_unknown_name_falls_back_to_the_default_band() {
assert!(decides_allow(&hook_at_level("banana", "echo hi > ./notes.txt").0), "unknown → default allows a write");
assert!(!decides_allow(&hook_at_level("banana", "git push origin main").0), "unknown → default still gates git push");
}
#[test]
fn claude_default_invocation_allows_safe_command() {
let payload = r#"{"tool_input": {"command": "ls -la"}}"#;
let (stdout, _stderr, code) = run_hook(&[], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(
v.pointer("/hookSpecificOutput/permissionDecision")
.and_then(|d| d.as_str()),
Some("allow"),
);
}
#[test]
fn claude_default_invocation_denies_unsafe_command() {
let payload = r#"{"tool_input": {"command": "git push"}}"#;
let (stdout, _stderr, code) = run_hook(&[], payload);
assert_eq!(code, 0);
assert_eq!(stdout, "");
}
#[test]
fn claude_nudges_when_a_command_reaches_outside_the_workspace() {
let payload = r#"{"tool_input": {"command": "cat /etc/hosts"}, "cwd": "/Users/me/proj"}"#;
let (stdout, _stderr, code) = run_hook(&[], payload);
assert_eq!(code, 0);
assert!(stdout.contains("additionalContext"), "expected a nudge: {stdout}");
assert!(!stdout.contains("permissionDecision"), "the nudge must not decide: {stdout}");
}
#[test]
fn claude_explicit_subcommand_allows_safe_command() {
let payload = r#"{"tool_input": {"command": "git status"}}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "claude"], payload);
assert_eq!(code, 0);
assert!(stdout.contains("\"permissionDecision\":\"allow\""));
}
#[test]
fn claude_invalid_json_exits_zero_silently() {
let (stdout, _stderr, code) = run_hook(&[], "not json at all");
assert_eq!(code, 0);
assert_eq!(stdout, "");
}
#[test]
fn claude_safewrite_carries_appropriate_reason() {
let payload = r#"{"tool_input": {"command": "cargo build"}}"#;
let (stdout, _stderr, code) = run_hook(&[], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect(&stdout);
let reason = v
.pointer("/hookSpecificOutput/permissionDecisionReason")
.and_then(|s| s.as_str())
.unwrap_or("");
assert!(
reason.contains("safe utilities"),
"reason was: {reason}"
);
}
#[test]
fn codex_hook_safe_command_emits_nothing() {
let payload = r#"{"tool_name": "Bash", "tool_input": {"command": "ls -la"}}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "codex"], payload);
assert_eq!(code, 0);
assert_eq!(stdout, "");
}
#[test]
fn codex_hook_denies_gated_command() {
let payload = r#"{"tool_name": "Bash", "tool_input": {"command": "rm -rf /etc"}}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "codex"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(
v.pointer("/hookSpecificOutput/permissionDecision").and_then(|d| d.as_str()),
Some("deny"),
);
assert_eq!(
v.pointer("/hookSpecificOutput/hookEventName").and_then(|d| d.as_str()),
Some("PreToolUse"),
);
}
#[test]
fn codex_hook_handles_optional_cwd() {
let payload = r#"{"tool_input": {"command": "git status"}, "cwd": "/Users/me/project"}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "codex"], payload);
assert_eq!(code, 0);
assert_eq!(stdout, "");
}
#[test]
fn codex_hook_invalid_json_exits_zero_silently() {
let (stdout, _stderr, code) = run_hook(&["hook", "codex"], "{");
assert_eq!(code, 0);
assert_eq!(stdout, "");
}
#[test]
fn codex_hook_gated_overreach_reason_names_the_path() {
let payload = r#"{"tool_input": {"command": "cat /etc/hosts"}, "cwd": "/tmp/proj"}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "codex"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
let reason = v
.pointer("/hookSpecificOutput/permissionDecisionReason")
.and_then(|d| d.as_str())
.unwrap_or_default();
assert!(reason.contains("/etc/hosts"), "reason should name the path: {reason}");
assert!(reason.contains("outside the working directory"), "reason: {reason}");
}
#[test]
fn codex_hook_gated_non_overreach_uses_generic_reason() {
let payload = r#"{"tool_input": {"command": "frobnicate --wizz"}, "cwd": "/tmp/proj"}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "codex"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
let reason = v
.pointer("/hookSpecificOutput/permissionDecisionReason")
.and_then(|d| d.as_str())
.unwrap_or_default();
assert!(reason.contains("not on the allowlist"), "reason: {reason}");
assert!(!reason.contains("outside the working directory"), "reason: {reason}");
}
#[test]
fn antigravity_hook_safe_command_allows() {
let payload = r#"{"toolCall":{"name":"run_command","args":{"CommandLine":"git status"}},"workspacePaths":["/tmp/proj"]}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "antigravity"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(v.get("decision").and_then(|d| d.as_str()), Some("allow"));
}
#[test]
fn antigravity_hook_gated_command_force_asks() {
let payload = r#"{"toolCall":{"name":"run_command","args":{"CommandLine":"frobnicate --wizz"}},"workspacePaths":["/tmp/proj"]}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "antigravity"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(v.get("decision").and_then(|d| d.as_str()), Some("force_ask"));
let reason = v.get("reason").and_then(|d| d.as_str()).unwrap_or_default();
assert!(reason.contains("stops flagging it"), "reason: {reason}");
}
#[test]
fn antigravity_hook_gated_overreach_force_asks_with_path() {
let payload = r#"{"toolCall":{"name":"run_command","args":{"CommandLine":"cat /etc/hosts"}},"workspacePaths":["/tmp/proj"]}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "antigravity"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(v.get("decision").and_then(|d| d.as_str()), Some("force_ask"));
let reason = v.get("reason").and_then(|d| d.as_str()).unwrap_or_default();
assert!(reason.contains("/etc/hosts"), "reason should name the path: {reason}");
assert!(reason.contains("outside the working directory"), "reason: {reason}");
}
#[test]
fn antigravity_hook_credential_reach_names_credential_store() {
let payload = r#"{"toolCall":{"name":"run_command","args":{"CommandLine":"cat ~/.ssh/id_rsa"}},"workspacePaths":["/tmp/proj"]}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "antigravity"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(v.get("decision").and_then(|d| d.as_str()), Some("force_ask"));
let reason = v.get("reason").and_then(|d| d.as_str()).unwrap_or_default();
assert!(reason.contains("credential store"), "reason: {reason}");
}
#[test]
fn unknown_tool_in_hook_subcommand_errors() {
let (_stdout, stderr, code) = run_hook(&["hook", "made-up-tool"], "{}");
assert_eq!(code, 1);
assert!(
stderr.contains("Unknown tool"),
"stderr was: {stderr}"
);
}
#[test]
fn list_tools_includes_every_supported_target() {
let out = Command::new(binary())
.arg("--list-tools")
.output()
.expect("run");
let s = String::from_utf8_lossy(&out.stdout);
for tool in ["claude", "codex", "cursor", "gemini", "copilot", "qwen", "droid", "opencode"] {
assert!(s.contains(tool), "list-tools missing `{tool}`: {s}");
}
}
#[test]
fn cursor_hook_allows_safe_command() {
let payload = r#"{
"conversation_id": "abc-123",
"generation_id": "gen-456",
"model": "claude-sonnet-4-5",
"hook_event_name": "beforeShellExecution",
"cursor_version": "2.0.43",
"workspace_roots": ["/Users/me/project"],
"user_email": null,
"transcript_path": null,
"command": "ls -la",
"cwd": "/Users/me/project",
"sandbox": false
}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "cursor"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(
v.get("permission").and_then(|s| s.as_str()),
Some("allow"),
);
assert!(v.get("permissionDecision").is_none(), "no Claude wrapper");
assert!(v.get("decision").is_none(), "no Gemini wrapper");
}
#[test]
fn cursor_hook_denies_unsafe_command() {
let payload = r#"{"command": "rm -rf /etc", "cwd": "/x"}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "cursor"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(v.get("permission").and_then(|s| s.as_str()), Some("deny"));
assert!(v.get("user_message").and_then(|s| s.as_str()).is_some_and(|m| m.contains("safe-chains")));
}
#[test]
fn gemini_hook_allows_safe_command() {
let payload = r#"{
"session_id": "abc",
"transcript_path": "/t",
"cwd": "/p",
"hook_event_name": "BeforeTool",
"timestamp": "2026-05-06T12:00:00Z",
"tool_name": "run_shell_command",
"tool_input": {"command": "ls -la"}
}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "gemini"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(v.get("decision").and_then(|s| s.as_str()), Some("allow"));
assert!(v.get("permission").is_none(), "no Cursor wrapper");
assert!(v.get("permissionDecision").is_none(), "no Claude wrapper");
}
#[test]
fn gemini_hook_skips_non_shell_tool() {
let payload = r#"{"tool_name": "list_files", "tool_input": {"command": "ignored"}}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "gemini"], payload);
assert_eq!(code, 0);
assert_eq!(stdout, "");
}
#[test]
fn qwen_hook_allows_safe_command() {
let payload = r#"{
"session_id": "abc",
"transcript_path": "/t",
"cwd": "/p",
"hook_event_name": "PreToolUse",
"timestamp": "2026-05-06T12:00:00Z",
"permission_mode": "default",
"tool_name": "Bash",
"tool_input": {"command": "ls -la"},
"tool_use_id": "tu_1"
}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "qwen"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(
v.pointer("/hookSpecificOutput/permissionDecision")
.and_then(|d| d.as_str()),
Some("allow"),
);
}
#[test]
fn droid_hook_allows_safe_command() {
let payload = r#"{
"session_id": "abc",
"transcript_path": "/t",
"cwd": "/p",
"permission_mode": "off",
"hook_event_name": "PreToolUse",
"tool_name": "Execute",
"tool_input": {"command": "ls -la"}
}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "droid"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(
v.pointer("/hookSpecificOutput/permissionDecision")
.and_then(|d| d.as_str()),
Some("allow"),
);
}
#[test]
fn copilot_hook_allows_safe_command_via_double_decode() {
let payload = r#"{
"timestamp": 1704614600000,
"cwd": "/path/to/project",
"toolName": "bash",
"toolArgs": "{\"command\":\"ls -la\",\"description\":\"list files\"}"
}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "copilot"], payload);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(
v.get("permissionDecision").and_then(|s| s.as_str()),
Some("allow"),
);
assert!(
v.get("hookSpecificOutput").is_none(),
"Copilot uses flat output — no wrapper",
);
}
#[test]
fn copilot_hook_skips_non_bash_tools() {
let payload = r#"{
"timestamp": 1,
"cwd": "/p",
"toolName": "edit",
"toolArgs": "{\"path\":\"x\"}"
}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "copilot"], payload);
assert_eq!(code, 0);
assert_eq!(stdout, "");
}
#[test]
fn copilot_hook_denies_unsafe_via_double_decode() {
let payload = r#"{
"timestamp": 1,
"cwd": "/p",
"toolName": "bash",
"toolArgs": "{\"command\":\"rm -rf /etc\"}"
}"#;
let (stdout, _stderr, code) = run_hook(&["hook", "copilot"], payload);
assert_eq!(code, 0);
assert_eq!(stdout, "");
}
#[test]
fn every_target_emits_its_decision_at_the_declared_field() {
use safe_chains::Verdict;
let mut failures = Vec::new();
let mut checked = 0usize;
let pointers: Vec<&'static str> = safe_chains::targets::registry()
.iter()
.filter_map(|t| t.hook_format().map(|f| f.decision_pointer()))
.collect();
for target in safe_chains::targets::registry() {
let Some(fmt) = target.hook_format() else { continue };
let name = target.name();
let mine = fmt.decision_pointer();
let emissions = [
("render_response(Allowed)", fmt.render_response(Verdict::Allowed(safe_chains::SafetyLevel::Inert)).stdout),
("render_deny", fmt.render_deny("blocked").stdout),
("render_ask", fmt.render_ask("confirm").stdout),
];
for (what, stdout) in emissions {
if stdout.trim().is_empty() {
continue; }
let Ok(v) = serde_json::from_str::<serde_json::Value>(&stdout) else {
failures.push(format!("{name}: {what} emitted non-JSON `{stdout}`"));
continue;
};
checked += 1;
if v.pointer(mine).and_then(|d| d.as_str()).is_none() {
failures.push(format!(
"{name}: {what} has no decision at its declared pointer `{mine}` -> `{stdout}`"
));
}
for other in &pointers {
if *other != mine && v.pointer(other).is_some() {
failures.push(format!(
"{name}: {what} ALSO decides at `{other}`, another harness's field -> `{stdout}`"
));
}
}
}
}
assert!(checked > 0, "no target emitted a decision — the guard is vacuous");
assert!(failures.is_empty(), "decision-field contract violations:\n{}", failures.join("\n"));
}
#[test]
fn no_target_install_panics_or_clobbers_an_unreadable_config() {
use serde_json::Value;
fn corruptions(installed: &Value) -> Vec<String> {
let mut out = vec!["\"just a string\"".to_string(), "[]".to_string(), "42".to_string()];
if let Some(obj) = installed.as_object() {
for (k, v) in obj {
let mut c = installed.clone();
c[k] = Value::String("corrupted".into());
out.push(c.to_string());
for inner in v.as_object().into_iter().flatten().map(|(ik, _)| ik) {
let mut c = installed.clone();
c[k][inner] = Value::String("corrupted".into());
out.push(c.to_string());
}
}
}
out
}
let mut exercised = 0;
let mut failures = Vec::new();
for target in safe_chains::targets::registry() {
let home = tempfile::tempdir().expect("tempdir");
for dir in target.detect_paths(home.path()) {
let _ = std::fs::create_dir_all(&dir);
}
let Ok(safe_chains::targets::InstallOutcome::Installed { path }) = target.install(home.path())
else {
continue;
};
let Ok(text) = std::fs::read_to_string(&path) else { continue };
let Ok(installed): Result<Value, _> = serde_json::from_str(&text) else { continue };
for corrupt in corruptions(&installed) {
std::fs::write(&path, &corrupt).expect("write corrupt config");
let before = std::fs::read(&path).expect("read back");
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
target.install(home.path())
}));
let name = target.name();
match result {
Err(_) => failures.push(format!("{name}: PANICKED on config `{corrupt}`")),
Ok(Err(_)) => {
let after = std::fs::read(&path).expect("read after");
if before != after {
failures.push(format!("{name}: refused but still rewrote `{corrupt}`"));
}
}
Ok(Ok(_)) => {
let after = std::fs::read_to_string(&path).expect("read after");
if serde_json::from_str::<Value>(&after).is_err() {
failures.push(format!("{name}: wrote invalid JSON from `{corrupt}`"));
}
}
}
exercised += 1;
}
}
assert!(exercised > 0, "no target install was exercised; the guard would be vacuous");
assert!(failures.is_empty(), "install robustness:\n {}", failures.join("\n "));
}
#[test]
fn the_decision_seam_never_grants_on_a_blank_command() {
use safe_chains::targets::{may_grant, respond};
use safe_chains::Verdict;
let safe = safe_chains::command_verdict("ls");
assert!(safe.is_allowed(), "precondition: `ls` classifies safe");
for blank in ["", " ", "\t", "\n", " \t \n "] {
assert!(!may_grant(blank, safe), "blank {blank:?} must not be grantable");
}
assert!(may_grant("ls", safe), "a real safe command must stay grantable");
assert!(!may_grant("ls", Verdict::Denied), "a denied verdict must never grant");
let mut checked = 0;
for target in safe_chains::targets::registry() {
let Some(format) = target.hook_format() else { continue };
checked += 1;
for blank in ["", " ", "\n"] {
assert!(
respond(format, blank, safe).is_none(),
"{}: respond() produced a decision for a blank command",
target.name()
);
}
let real = respond(format, "ls", safe);
assert!(
real.is_some(),
"{}: respond() produced nothing for a real safe command; the blank check above proves \
nothing",
target.name()
);
}
assert!(checked > 0, "no hook target was exercised; the guard would be vacuous");
}
#[test]
fn no_target_approves_a_blank_command() {
const ENVELOPES: &[(&str, &str)] = &[
("claude", r#"{"tool_name":"Bash","tool_input":{"command":"{CMD}"}}"#),
("codex", r#"{"tool_name":"Bash","tool_input":{"command":"{CMD}"}}"#),
("gemini", r#"{"tool_name":"Bash","tool_input":{"command":"{CMD}"}}"#),
("qwen", r#"{"tool_name":"Bash","tool_input":{"command":"{CMD}"}}"#),
("droid", r#"{"tool_name":"Bash","tool_input":{"command":"{CMD}"}}"#),
("copilot", r#"{"toolName":"Bash","toolArgs":"{\"command\":\"{CMD}\"}"}"#),
(
"antigravity",
r#"{"toolCall":{"name":"Bash","args":{"CommandLine":"{CMD}"}},"workspacePaths":["/w"]}"#,
),
("cursor", r#"{"command":"{CMD}","cwd":"/w","workspace_roots":["/w"]}"#),
("grok", r#"{"tool_input":{"command":"{CMD}"},"cwd":"/w"}"#),
];
let mut failures = Vec::new();
for (name, template) in ENVELOPES {
for blank in ["", " "] {
let envelope = template.replace("{CMD}", blank);
let (stdout, _stderr, code) = run_hook(&["hook", name], &envelope);
if code != 0 {
failures.push(format!("{name}: blank command exited {code} (a crash fails OPEN)"));
}
for token in ["\"allow\"", "\"approve\""] {
if stdout.contains(token) {
failures.push(format!("{name}: blank command approved -> `{stdout}`"));
}
}
}
let (stdout, _, _) = run_hook(&["hook", name], &template.replace("{CMD}", "ls"));
if stdout.trim().is_empty() && matches!(*name, "claude" | "qwen" | "cursor") {
failures.push(format!("{name}: envelope no longer parses; the blank check is vacuous"));
}
}
for target in safe_chains::targets::registry() {
if target.hook_format().is_some() && !ENVELOPES.iter().any(|(n, _)| *n == target.name()) {
failures.push(format!(
"{}: hook target missing from this guard's envelope table",
target.name()
));
}
}
assert!(failures.is_empty(), "blank-command contract:\n {}", failures.join("\n "));
}
#[test]
fn every_target_hook_contract_is_fail_safe() {
use safe_chains::Verdict;
let mut failures = Vec::new();
for target in safe_chains::targets::registry() {
let Some(fmt) = target.hook_format() else {
continue;
};
let name = target.name();
let denied = fmt.render_response(Verdict::Denied);
if !denied.stdout.trim().is_empty() {
failures.push(format!("{name}: render_response(Denied) emitted `{}`", denied.stdout));
}
let ctx = fmt.render_context("reaches /etc/x outside your workspace");
for key in ["\"permissionDecision\"", "\"decision\"", "\"permission\""] {
if ctx.stdout.contains(key) {
failures.push(format!("{name}: render_context leaked {key} -> `{}`", ctx.stdout));
}
}
if fmt.parse_input("this is not json {[").is_ok() {
failures.push(format!("{name}: parse_input accepted garbage as a command"));
}
use safe_chains::targets::GatedPolicy;
match fmt.gated_policy() {
GatedPolicy::Deny => {
if !fmt.render_deny("blocked").stdout.contains("\"deny\"") {
failures.push(format!("{name}: Deny policy but render_deny has no deny decision"));
}
if !fmt.render_ask("x").stdout.trim().is_empty() {
failures.push(format!("{name}: Deny policy but render_ask is non-empty"));
}
}
GatedPolicy::Ask => {
let ask = fmt.render_ask("confirm").stdout;
if !ask.contains("\"ask\"") && !ask.contains("\"force_ask\"") {
failures.push(format!("{name}: Ask policy but render_ask has no ask decision"));
}
if !fmt.render_deny("x").stdout.trim().is_empty() {
failures.push(format!("{name}: Ask policy but render_deny is non-empty"));
}
}
GatedPolicy::Defer => {
if !fmt.render_deny("x").stdout.trim().is_empty() || !fmt.render_ask("x").stdout.trim().is_empty() {
failures.push(format!("{name}: Defer policy but a render_deny/ask emitted output"));
}
}
}
}
assert!(failures.is_empty(), "target hook contract violations:\n{}", failures.join("\n"));
}