use anyhow::{Context, Result};
use tracing::{Level, info, instrument};
use crate::cli::HooksCmd;
use crate::hooks::{HookOutput, HookSpecificOutput, ToolUseHookInput, is_interactive_tool};
use crate::permissions::check_permission;
use crate::policy::Effect;
use crate::session_policy;
use crate::settings::{ClashSettings, HookContext};
use crate::trace;
use claude_settings::PermissionRule;
impl HooksCmd {
fn run_disabled(&self) -> Result<()> {
info!("Clash is disabled (CLASH_DISABLE), returning pass-through");
let output = match self {
Self::SessionStart => {
let _ = crate::hooks::SessionStartHookInput::from_reader(std::io::stdin().lock());
HookOutput::session_start(Some(
"Clash is disabled (CLASH_DISABLE is set). \
All hooks are pass-through — no policy enforcement is active. \
Unset CLASH_DISABLE to re-enable."
.into(),
))
}
_ => {
let _ = std::io::copy(&mut std::io::stdin().lock(), &mut std::io::sink());
HookOutput::continue_execution()
}
};
output
.write_stdout()
.context("serializing disabled-mode hook response to stdout")?;
Ok(())
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn run(&self) -> Result<()> {
if crate::settings::is_disabled() {
return self.run_disabled();
}
let passthrough = crate::settings::is_passthrough();
let output = match self {
Self::PreToolUse => {
let input = ToolUseHookInput::from_reader(std::io::stdin().lock())
.context("parsing PreToolUse hook input from stdin — expected JSON with tool_name and tool_input fields")?;
if passthrough {
info!(
tool = %input.tool_name,
"CLASH_PASSTHROUGH: deferring to native permissions"
);
if let Err(e) = trace::sync_trace(&input.session_id, None) {
tracing::warn!(error = %e, "Failed to sync trace (PreToolUse/passthrough)");
}
HookOutput::continue_execution()
} else {
let hook_ctx = HookContext::from_transcript_path(&input.transcript_path);
let settings = ClashSettings::load_or_create_with_session(
Some(&input.session_id),
Some(&hook_ctx),
)?;
let output = check_permission(&input, &settings)?;
if is_interactive_tool(&input.tool_name) && !is_deny_decision(&output) {
info!(tool = %input.tool_name, "Passthrough: interactive tool deferred to Claude Code");
HookOutput::continue_execution()
} else {
if let Some(effect) = extract_effect(&output) {
crate::audit::update_session_stats(
&input.session_id,
&input.tool_name,
&input.tool_input,
effect,
&input.cwd,
);
}
if is_ask_decision(&output)
&& let Some(ref tool_use_id) = input.tool_use_id
{
session_policy::record_pending_ask(
&input.session_id,
tool_use_id,
&input.tool_name,
&input.tool_input,
&input.cwd,
);
}
let decision = input.tool_use_id.as_ref().and_then(|id| {
let effect = extract_effect(&output)?;
Some(trace::PolicyDecision {
tool_use_id: id.clone(),
tool_name: Some(input.tool_name.clone()),
effect,
reason: None,
})
});
if let Err(e) = trace::sync_trace(&input.session_id, decision) {
tracing::warn!(error = %e, "Failed to sync trace (PreToolUse)");
}
output
}
}
}
Self::PostToolUse => {
let input = ToolUseHookInput::from_reader(std::io::stdin().lock())
.context("parsing PostToolUse hook input from stdin")?;
let session_context = input.tool_use_id.as_deref().and_then(|tool_use_id| {
let advice = session_policy::process_post_tool_use(
tool_use_id,
&input.session_id,
&input.tool_name,
&input.tool_input,
&input.cwd,
)?;
info!(
rule = %advice.suggested_rule,
"Suggesting session rule for user approval"
);
Some(advice.as_context())
});
let (network_context, fs_context) = {
let hook_ctx = HookContext::from_transcript_path(&input.transcript_path);
let settings = ClashSettings::load_or_create_with_session(
Some(&input.session_id),
Some(&hook_ctx),
)
.ok();
let net = settings.as_ref().and_then(|s| {
crate::network_hints::check_for_sandbox_network_hint(&input, s)
});
let fs = settings
.as_ref()
.and_then(|s| crate::sandbox_hints::check_for_sandbox_fs_hint(&input, s));
(net, fs)
};
let context = [session_context, network_context, fs_context]
.into_iter()
.flatten()
.collect::<Vec<_>>();
let context = if context.is_empty() {
None
} else {
Some(context.join("\n\n"))
};
if let Err(e) = trace::sync_trace(&input.session_id, None) {
tracing::warn!(error = %e, "Failed to sync trace (PostToolUse)");
}
HookOutput::post_tool_use(context)
}
Self::PermissionRequest => {
let input = ToolUseHookInput::from_reader(std::io::stdin().lock())
.context("parsing PermissionRequest hook input from stdin")?;
if passthrough {
info!(
tool = %input.tool_name,
"CLASH_PASSTHROUGH: deferring permission request to native UI"
);
HookOutput::continue_execution()
} else {
let hook_ctx = HookContext::from_transcript_path(&input.transcript_path);
let settings = ClashSettings::load_or_create_with_session(
Some(&input.session_id),
Some(&hook_ctx),
)?;
crate::handlers::handle_permission_request(&input, &settings)?
}
}
Self::SessionStart => {
let input =
crate::hooks::SessionStartHookInput::from_reader(std::io::stdin().lock())
.context("parsing SessionStart hook input from stdin")?;
crate::handlers::handle_session_start(&input)?
}
Self::Stop => {
let input = crate::hooks::StopHookInput::from_reader(std::io::stdin().lock())
.context("parsing Stop hook input from stdin")?;
if let Err(e) = trace::sync_trace(&input.session_id, None) {
tracing::warn!(error = %e, "Failed to sync trace (Stop)");
}
HookOutput::continue_execution()
}
};
output
.write_stdout()
.context("serializing hook response to stdout")?;
Ok(())
}
}
fn extract_effect(output: &HookOutput) -> Option<Effect> {
match &output.hook_specific_output {
Some(HookSpecificOutput::PreToolUse(pre)) => match pre.permission_decision {
Some(PermissionRule::Allow) => Some(Effect::Allow),
Some(PermissionRule::Deny) => Some(Effect::Deny),
Some(PermissionRule::Ask) => Some(Effect::Ask),
Some(PermissionRule::Unset) | None => None,
},
_ => None,
}
}
fn is_ask_decision(output: &HookOutput) -> bool {
matches!(extract_effect(output), Some(Effect::Ask))
}
fn is_deny_decision(output: &HookOutput) -> bool {
matches!(extract_effect(output), Some(Effect::Deny))
}