use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct SpawnOpts {
pub session_id: String,
pub permission_mode: Option<String>,
pub dangerous: bool,
}
pub trait Backend {
fn name(&self) -> &'static str;
fn spawn_command(&self, opts: &SpawnOpts) -> String;
fn plan_markers(&self) -> &'static [&'static str];
fn permission_markers(&self) -> &'static [&'static str];
fn trust_markers(&self) -> &'static [&'static str];
fn verified_versions(&self) -> &'static [&'static str];
fn installed_version(&self) -> Option<String>;
}
pub fn marker_warning(backend: &dyn Backend, installed: Option<&str>) -> Option<String> {
let extra = std::env::var("CSD_VERIFIED_VERSIONS").unwrap_or_default();
marker_warning_with(backend.name(), backend.verified_versions(), installed, &extra)
}
fn marker_warning_with(backend_name: &str, verified: &[&str], installed: Option<&str>, extra: &str) -> Option<String> {
let Some(installed) = installed else {
return Some(format!(
"could not determine the installed {backend_name} version; gate markers were \
verified on {verified:?} and may not match — gate detection \
(plan/permission/trust) can silently miss prompts"
));
};
let extra_match = extra.split(',').map(str::trim).any(|v| !v.is_empty() && v == installed);
if verified.contains(&installed) || extra_match {
return None;
}
Some(format!(
"{backend_name} {installed} is not marker-verified (verified: {verified:?}); gate \
detection (plan/permission/trust) may silently miss prompts. If gates still work on \
{installed}, set CSD_VERIFIED_VERSIONS={installed} to silence this warning"
))
}
fn parse_version_output(stdout: &str) -> Option<String> {
let token = stdout.split_whitespace().next()?;
token
.starts_with(|c: char| c.is_ascii_digit())
.then(|| token.to_string())
}
pub const CLAUDE_PERMISSION_MODES: &[&str] =
&["plan", "acceptEdits", "auto", "bypassPermissions", "default", "dontAsk"];
pub struct Claude;
impl Backend for Claude {
fn name(&self) -> &'static str {
"claude"
}
fn spawn_command(&self, opts: &SpawnOpts) -> String {
let mut cmd = format!(
"env -u CLAUDECODE -u CLAUDE_CODE_ENTRYPOINT claude --session-id {}",
opts.session_id
);
if let Some(mode) = &opts.permission_mode {
cmd.push_str(" --permission-mode ");
cmd.push_str(mode);
}
if opts.dangerous {
cmd.push_str(" --dangerously-skip-permissions");
}
cmd
}
fn plan_markers(&self) -> &'static [&'static str] {
&["Here is Claude's plan", "Would you like to proceed?"]
}
fn permission_markers(&self) -> &'static [&'static str] {
&["Do you want to proceed?", "Do you want to make this edit?"]
}
fn trust_markers(&self) -> &'static [&'static str] {
&[
"Is this a project you created or one you trust?",
"Yes, I trust this folder",
]
}
fn verified_versions(&self) -> &'static [&'static str] {
&["2.1.158", "2.1.173"]
}
fn installed_version(&self) -> Option<String> {
let output = std::process::Command::new("claude").arg("--version").output().ok()?;
if !output.status.success() {
return None;
}
parse_version_output(&String::from_utf8_lossy(&output.stdout))
}
}
pub fn resolve(name: &str) -> Result<Box<dyn Backend>> {
match name {
"claude" => Ok(Box::new(Claude)),
other => Err(Error::UnknownBackend(other.to_string())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_claude_version_output() {
assert_eq!(
parse_version_output("2.1.173 (Claude Code)\n"),
Some("2.1.173".to_string())
);
assert_eq!(parse_version_output("2.1.158"), Some("2.1.158".to_string()));
assert_eq!(parse_version_output("Claude Code 2.1.173"), None);
assert_eq!(parse_version_output(""), None);
assert_eq!(parse_version_output(" \n"), None);
}
#[test]
fn no_warning_on_verified_version() {
assert_eq!(marker_warning_with("claude", &["2.1.158"], Some("2.1.158"), ""), None);
}
#[test]
fn warns_on_unverified_version() {
let warning = marker_warning_with("claude", &["2.1.158"], Some("2.1.173"), "").unwrap();
assert!(warning.contains("2.1.173"));
assert!(warning.contains("2.1.158"));
assert!(warning.contains("CSD_VERIFIED_VERSIONS=2.1.173"));
}
#[test]
fn warns_on_unknown_version() {
let warning = marker_warning_with("claude", &["2.1.158"], None, "").unwrap();
assert!(warning.contains("could not determine"));
}
#[test]
fn env_override_extends_verified_set() {
let extra = "2.1.170, 2.1.173";
assert_eq!(
marker_warning_with("claude", &["2.1.158"], Some("2.1.173"), extra),
None
);
assert_eq!(
marker_warning_with("claude", &["2.1.158"], Some("2.1.170"), extra),
None
);
assert!(marker_warning_with("claude", &["2.1.158"], Some("2.1.171"), extra).is_some());
assert!(marker_warning_with("claude", &["2.1.158"], Some(""), ",").is_some());
}
}