diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs
index 2fb18f4..fc89406 100644
--- a/src-tauri/src/config.rs
+++ b/src-tauri/src/config.rs
@@ -54,6 +54,9 @@ pub struct ClaudeStartOptions {
#[serde(default)]
pub session_name: Option,
+
+ #[serde(default)]
+ pub disable_skill_shell_execution: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -203,6 +206,11 @@ pub struct HikariConfig {
#[serde(default)]
pub model_overrides: Option>,
+
+ /// Prevents skill scripts from executing shell commands (Claude Code v2.1.91+).
+ /// Passes `"disableSkillShellExecution": true` via the `--settings` flag.
+ #[serde(default)]
+ pub disable_skill_shell_execution: bool,
}
impl Default for HikariConfig {
@@ -254,6 +262,7 @@ impl Default for HikariConfig {
enable_claudeai_mcp_servers: true,
auto_memory_directory: None,
model_overrides: None,
+ disable_skill_shell_execution: false,
}
}
}
@@ -405,6 +414,7 @@ mod tests {
assert!(config.enable_claudeai_mcp_servers);
assert!(config.auto_memory_directory.is_none());
assert!(config.model_overrides.is_none());
+ assert!(!config.disable_skill_shell_execution);
}
#[test]
@@ -459,6 +469,7 @@ mod tests {
"claude-opus-4-6".to_string(),
"arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-6-v1".to_string(),
)])),
+ disable_skill_shell_execution: true,
};
let json = serde_json::to_string(&config).unwrap();
diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs
index 73a1f57..097b67f 100644
--- a/src-tauri/src/types.rs
+++ b/src-tauri/src/types.rs
@@ -318,6 +318,42 @@ pub struct PostCompactEvent {
pub conversation_id: Option,
}
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CwdChangedEvent {
+ pub cwd: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub conversation_id: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct FileChangedEvent {
+ pub file: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub conversation_id: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TaskCreatedEvent {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub task_id: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub description: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub parent_tool_use_id: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub conversation_id: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PermissionDeniedEvent {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub tool_name: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub reason: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub conversation_id: Option,
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentStartEvent {
pub tool_use_id: String,
@@ -741,4 +777,137 @@ mod tests {
assert!(serialized.contains("\"session_id\":\"sess-xyz\""));
assert!(!serialized.contains("conversation_id"));
}
+
+ #[test]
+ fn test_cwd_changed_event_serialization() {
+ let event = CwdChangedEvent {
+ cwd: "/home/naomi/code/my-project".to_string(),
+ conversation_id: Some("conv-abc".to_string()),
+ };
+
+ let serialized = serde_json::to_string(&event).unwrap();
+ assert!(serialized.contains("\"cwd\":\"/home/naomi/code/my-project\""));
+ assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
+ }
+
+ #[test]
+ fn test_cwd_changed_event_omits_none_fields() {
+ let event = CwdChangedEvent {
+ cwd: "/tmp/workspace".to_string(),
+ conversation_id: None,
+ };
+
+ let serialized = serde_json::to_string(&event).unwrap();
+ assert!(serialized.contains("\"cwd\":\"/tmp/workspace\""));
+ assert!(!serialized.contains("conversation_id"));
+ }
+
+ #[test]
+ fn test_file_changed_event_serialization() {
+ let event = FileChangedEvent {
+ file: "/home/naomi/code/my-project/src/main.rs".to_string(),
+ conversation_id: Some("conv-abc".to_string()),
+ };
+
+ let serialized = serde_json::to_string(&event).unwrap();
+ assert!(serialized.contains("\"file\":\"/home/naomi/code/my-project/src/main.rs\""));
+ assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
+ }
+
+ #[test]
+ fn test_file_changed_event_omits_none_fields() {
+ let event = FileChangedEvent {
+ file: "/tmp/test.txt".to_string(),
+ conversation_id: None,
+ };
+
+ let serialized = serde_json::to_string(&event).unwrap();
+ assert!(serialized.contains("\"file\":\"/tmp/test.txt\""));
+ assert!(!serialized.contains("conversation_id"));
+ }
+
+ #[test]
+ fn test_task_created_event_serialization() {
+ let event = TaskCreatedEvent {
+ task_id: Some("task-abc123".to_string()),
+ description: Some("Explore the codebase".to_string()),
+ parent_tool_use_id: Some("toolu_xyz".to_string()),
+ conversation_id: Some("conv-abc".to_string()),
+ };
+
+ let serialized = serde_json::to_string(&event).unwrap();
+ assert!(serialized.contains("\"task_id\":\"task-abc123\""));
+ assert!(serialized.contains("\"description\":\"Explore the codebase\""));
+ assert!(serialized.contains("\"parent_tool_use_id\":\"toolu_xyz\""));
+ assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
+ }
+
+ #[test]
+ fn test_task_created_event_omits_none_fields() {
+ let event = TaskCreatedEvent {
+ task_id: None,
+ description: None,
+ parent_tool_use_id: None,
+ conversation_id: None,
+ };
+
+ let serialized = serde_json::to_string(&event).unwrap();
+ assert_eq!(serialized, "{}");
+ }
+
+ #[test]
+ fn test_task_created_event_partial_fields() {
+ let event = TaskCreatedEvent {
+ task_id: Some("task-001".to_string()),
+ description: None,
+ parent_tool_use_id: None,
+ conversation_id: None,
+ };
+
+ let serialized = serde_json::to_string(&event).unwrap();
+ assert!(serialized.contains("\"task_id\":\"task-001\""));
+ assert!(!serialized.contains("description"));
+ assert!(!serialized.contains("parent_tool_use_id"));
+ assert!(!serialized.contains("conversation_id"));
+ }
+
+ #[test]
+ fn test_permission_denied_event_serialization() {
+ let event = PermissionDeniedEvent {
+ tool_name: Some("Bash".to_string()),
+ reason: Some("Tool not in allow list".to_string()),
+ conversation_id: Some("conv-abc".to_string()),
+ };
+
+ let serialized = serde_json::to_string(&event).unwrap();
+ assert!(serialized.contains("\"tool_name\":\"Bash\""));
+ assert!(serialized.contains("\"reason\":\"Tool not in allow list\""));
+ assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
+ }
+
+ #[test]
+ fn test_permission_denied_event_omits_none_fields() {
+ let event = PermissionDeniedEvent {
+ tool_name: None,
+ reason: None,
+ conversation_id: None,
+ };
+
+ let serialized = serde_json::to_string(&event).unwrap();
+ assert_eq!(serialized, "{}");
+ }
+
+ #[test]
+ fn test_permission_denied_event_partial_fields() {
+ let event = PermissionDeniedEvent {
+ tool_name: Some("Edit".to_string()),
+ reason: None,
+ conversation_id: None,
+ };
+
+ let serialized = serde_json::to_string(&event).unwrap();
+ assert!(serialized.contains("\"tool_name\":\"Edit\""));
+ assert!(!serialized.contains("reason"));
+ assert!(!serialized.contains("conversation_id"));
+ }
}
diff --git a/src-tauri/src/wsl_bridge.rs b/src-tauri/src/wsl_bridge.rs
index 1eef472..6a8f9a6 100644
--- a/src-tauri/src/wsl_bridge.rs
+++ b/src-tauri/src/wsl_bridge.rs
@@ -15,10 +15,11 @@ use crate::process_ext::HideWindow;
use crate::stats::{calculate_cost, StatsUpdateEvent, UsageStats};
use crate::types::{
AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent,
- ConnectionStatus, ContentBlock, ElicitationEvent, ElicitationResultEvent, MessageCost,
- OutputEvent, PermissionPromptEvent, PermissionPromptEventItem, QuestionOption, SessionEvent,
- PostCompactEvent, StateChangeEvent, StopFailureEvent, TodoItem, TodoUpdateEvent,
- UserQuestionEvent, WorkingDirectoryEvent, WorktreeEvent, WorktreeInfo,
+ ConnectionStatus, ContentBlock, CwdChangedEvent, ElicitationEvent, ElicitationResultEvent,
+ FileChangedEvent, MessageCost, OutputEvent, PermissionDeniedEvent, PermissionPromptEvent,
+ PermissionPromptEventItem, PostCompactEvent, QuestionOption, SessionEvent, StateChangeEvent,
+ StopFailureEvent, TaskCreatedEvent, TodoItem, TodoUpdateEvent, UserQuestionEvent,
+ WorkingDirectoryEvent, WorktreeEvent, WorktreeInfo,
};
use parking_lot::RwLock;
use std::cell::RefCell;
@@ -36,7 +37,10 @@ struct PendingToolUse {
tool_input: serde_json::Value,
}
-const SEARCH_TOOLS: [&str; 5] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch"];
+// "Monitor" added in Claude Code v2.1.98 — it streams events and is observational in nature,
+// so it maps to the Searching character state. It may appear as name "Monitor" in tool_use
+// blocks (confirmed by CLI source inspection; it is a local_bash with kind="monitor" internally).
+const SEARCH_TOOLS: [&str; 6] = ["Read", "Glob", "Grep", "WebSearch", "WebFetch", "Monitor"];
const CODING_TOOLS: [&str; 3] = ["Edit", "Write", "NotebookEdit"];
fn detect_wsl() -> bool {
@@ -312,7 +316,7 @@ impl WslBridge {
.map(|m| !m.is_empty())
.unwrap_or(false);
- if has_memory_dir || has_overrides {
+ if has_memory_dir || has_overrides || options.disable_skill_shell_execution {
let mut settings = serde_json::Map::new();
if let Some(ref dir) = options.auto_memory_directory {
if !dir.is_empty() {
@@ -329,6 +333,12 @@ impl WslBridge {
}
}
}
+ if options.disable_skill_shell_execution {
+ settings.insert(
+ "disableSkillShellExecution".to_string(),
+ serde_json::Value::Bool(true),
+ );
+ }
if let Ok(settings_json) = serde_json::to_string(&settings) {
cmd.args(["--settings", &settings_json]);
}
@@ -499,7 +509,7 @@ impl WslBridge {
.map(|m| !m.is_empty())
.unwrap_or(false);
- if has_memory_dir || has_overrides {
+ if has_memory_dir || has_overrides || options.disable_skill_shell_execution {
let mut settings = serde_json::Map::new();
if let Some(ref dir) = options.auto_memory_directory {
if !dir.is_empty() {
@@ -516,6 +526,12 @@ impl WslBridge {
}
}
}
+ if options.disable_skill_shell_execution {
+ settings.insert(
+ "disableSkillShellExecution".to_string(),
+ serde_json::Value::Bool(true),
+ );
+ }
if let Ok(settings_json) = serde_json::to_string(&settings) {
let escaped = settings_json.replace('\'', "'\\''");
claude_cmd.push_str(&format!(" --settings '{}'", escaped));
@@ -1078,6 +1094,10 @@ fn handle_stderr(
let is_elicitation_result = line.contains("[ElicitationResult Hook]");
let is_stop_failure = line.contains("[StopFailure Hook]");
let is_post_compact = line.contains("[PostCompact Hook]");
+ let is_cwd_changed = line.contains("[CwdChanged Hook]");
+ let is_file_changed = line.contains("[FileChanged Hook]");
+ let is_task_created = line.contains("[TaskCreated Hook]");
+ let is_permission_denied = line.contains("[PermissionDenied Hook]");
let line_type = if is_worktree_create || is_worktree_remove {
"worktree"
@@ -1089,6 +1109,14 @@ fn handle_stderr(
"error"
} else if is_post_compact {
"compact-prompt"
+ } else if is_cwd_changed {
+ "cwd-changed"
+ } else if is_file_changed {
+ "file-changed"
+ } else if is_task_created {
+ "task-created"
+ } else if is_permission_denied {
+ "permission-denied"
} else {
"error"
};
@@ -1227,6 +1255,124 @@ fn handle_stderr(
parent_tool_use_id: None,
},
);
+ } else if is_cwd_changed {
+ let data = parse_cwd_changed_hook(&line);
+ let friendly_content =
+ format!("Working directory changed to: {}", data.cwd);
+
+ let _ = app.emit(
+ "claude:cwd-changed",
+ CwdChangedEvent {
+ cwd: data.cwd,
+ conversation_id: conversation_id.clone(),
+ },
+ );
+
+ let _ = app.emit(
+ "claude:output",
+ OutputEvent {
+ line_type: "cwd-changed".to_string(),
+ content: friendly_content,
+ tool_name: None,
+ conversation_id: conversation_id.clone(),
+ cost: None,
+ parent_tool_use_id: None,
+ },
+ );
+ } else if is_file_changed {
+ let data = parse_file_changed_hook(&line);
+ let friendly_content = if data.file.is_empty() {
+ "File changed".to_string()
+ } else {
+ format!("File changed: {}", data.file)
+ };
+
+ let _ = app.emit(
+ "claude:file-changed",
+ FileChangedEvent {
+ file: data.file,
+ conversation_id: conversation_id.clone(),
+ },
+ );
+
+ let _ = app.emit(
+ "claude:output",
+ OutputEvent {
+ line_type: "file-changed".to_string(),
+ content: friendly_content,
+ tool_name: None,
+ conversation_id: conversation_id.clone(),
+ cost: None,
+ parent_tool_use_id: None,
+ },
+ );
+ } else if is_task_created {
+ let data = parse_task_created_hook(&line);
+ let friendly_content = match data.description.as_deref() {
+ Some(desc) if !desc.is_empty() => format!("Task created: {}", desc),
+ _ => "Task created".to_string(),
+ };
+
+ let _ = app.emit(
+ "claude:task-created",
+ TaskCreatedEvent {
+ task_id: data.task_id,
+ description: data.description,
+ parent_tool_use_id: data.parent_tool_use_id,
+ conversation_id: conversation_id.clone(),
+ },
+ );
+
+ let _ = app.emit(
+ "claude:output",
+ OutputEvent {
+ line_type: "task-created".to_string(),
+ content: friendly_content,
+ tool_name: None,
+ conversation_id: conversation_id.clone(),
+ cost: None,
+ parent_tool_use_id: None,
+ },
+ );
+ } else if is_permission_denied {
+ let data = parse_permission_denied_hook(&line);
+ let friendly_content = match (data.tool_name.as_deref(), data.reason.as_deref()) {
+ (Some(tool), Some(reason)) => {
+ format!("Permission denied for {}: {}", tool, reason)
+ }
+ (Some(tool), None) => format!("Permission denied for {}", tool),
+ _ => "Permission denied".to_string(),
+ };
+
+ let _ = app.emit(
+ "claude:state-change",
+ StateChangeEvent {
+ state: CharacterState::Permission,
+ tool_name: data.tool_name.clone(),
+ conversation_id: conversation_id.clone(),
+ },
+ );
+
+ let _ = app.emit(
+ "claude:permission-denied",
+ PermissionDeniedEvent {
+ tool_name: data.tool_name,
+ reason: data.reason,
+ conversation_id: conversation_id.clone(),
+ },
+ );
+
+ let _ = app.emit(
+ "claude:output",
+ OutputEvent {
+ line_type: "permission-denied".to_string(),
+ content: friendly_content,
+ tool_name: None,
+ conversation_id: conversation_id.clone(),
+ cost: None,
+ parent_tool_use_id: None,
+ },
+ );
} else {
let _ = app.emit(
"claude:output",
@@ -1440,6 +1586,85 @@ fn parse_post_compact_hook(line: &str) -> PostCompactData {
PostCompactData { session_id }
}
+#[derive(Debug)]
+struct CwdChangedData {
+ cwd: String,
+}
+
+fn parse_cwd_changed_hook(line: &str) -> CwdChangedData {
+ let cwd = extract_quoted_value(line, "cwd")
+ .or_else(|| extract_quoted_value(line, "path"))
+ .or_else(|| {
+ line.split("[CwdChanged Hook]")
+ .nth(1)
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+ })
+ .unwrap_or_default();
+ CwdChangedData { cwd }
+}
+
+#[derive(Debug)]
+struct FileChangedData {
+ file: String,
+}
+
+fn parse_file_changed_hook(line: &str) -> FileChangedData {
+ let file = extract_quoted_value(line, "file")
+ .or_else(|| extract_quoted_value(line, "path"))
+ .or_else(|| {
+ line.split("[FileChanged Hook]")
+ .nth(1)
+ .map(|s| s.trim().to_string())
+ .filter(|s| !s.is_empty())
+ })
+ .unwrap_or_default();
+ FileChangedData { file }
+}
+
+#[derive(Debug)]
+struct TaskCreatedData {
+ task_id: Option,
+ description: Option,
+ parent_tool_use_id: Option,
+}
+
+fn parse_task_created_hook(line: &str) -> TaskCreatedData {
+ let task_id = extract_debug_string_value(line, "task_id")
+ .or_else(|| extract_quoted_value(line, "task_id"));
+
+ let description = extract_quoted_value(line, "description")
+ .or_else(|| extract_quoted_value(line, "prompt"));
+
+ let parent_tool_use_id = if line.contains("parent_tool_use_id=Some") {
+ line.split("parent_tool_use_id=Some(\"")
+ .nth(1)
+ .and_then(|s| s.split('"').next())
+ .map(|s| s.to_string())
+ } else {
+ None
+ };
+
+ TaskCreatedData { task_id, description, parent_tool_use_id }
+}
+
+#[derive(Debug)]
+struct PermissionDeniedData {
+ tool_name: Option,
+ reason: Option,
+}
+
+fn parse_permission_denied_hook(line: &str) -> PermissionDeniedData {
+ let tool_name = extract_quoted_value(line, "tool_name")
+ .or_else(|| extract_quoted_value(line, "tool"))
+ .or_else(|| extract_debug_string_value(line, "tool_name"));
+
+ let reason = extract_quoted_value(line, "reason")
+ .or_else(|| extract_quoted_value(line, "message"));
+
+ PermissionDeniedData { tool_name, reason }
+}
+
/// Extracts a double-quoted string value from a `key="value"` pair in a hook line.
/// Handles escape sequences within the quoted value.
fn extract_quoted_value(line: &str, key: &str) -> Option {
@@ -2546,6 +2771,11 @@ mod tests {
get_tool_state("WebFetch"),
CharacterState::Searching
));
+ // Monitor tool added in v2.1.98 — observational/streaming, maps to Searching
+ assert!(matches!(
+ get_tool_state("Monitor"),
+ CharacterState::Searching
+ ));
}
#[test]
@@ -3588,6 +3818,130 @@ mod tests {
assert_eq!(data.session_id, None);
}
+ #[test]
+ fn test_parse_cwd_changed_hook_with_cwd_key() {
+ let line = r#"[CwdChanged Hook] cwd="/home/naomi/code/my-project""#;
+ let data = parse_cwd_changed_hook(line);
+ assert_eq!(data.cwd, "/home/naomi/code/my-project");
+ }
+
+ #[test]
+ fn test_parse_cwd_changed_hook_with_path_key() {
+ let line = r#"[CwdChanged Hook] path="/tmp/workspace""#;
+ let data = parse_cwd_changed_hook(line);
+ assert_eq!(data.cwd, "/tmp/workspace");
+ }
+
+ #[test]
+ fn test_parse_cwd_changed_hook_bare_path_fallback() {
+ let line = "[CwdChanged Hook] /home/naomi/code/project";
+ let data = parse_cwd_changed_hook(line);
+ assert_eq!(data.cwd, "/home/naomi/code/project");
+ }
+
+ #[test]
+ fn test_parse_cwd_changed_hook_empty_line() {
+ let line = "[CwdChanged Hook]";
+ let data = parse_cwd_changed_hook(line);
+ assert_eq!(data.cwd, "");
+ }
+
+ #[test]
+ fn test_parse_file_changed_hook_with_file_key() {
+ let line = r#"[FileChanged Hook] file="/home/naomi/code/project/src/main.rs""#;
+ let data = parse_file_changed_hook(line);
+ assert_eq!(data.file, "/home/naomi/code/project/src/main.rs");
+ }
+
+ #[test]
+ fn test_parse_file_changed_hook_with_path_key() {
+ let line = r#"[FileChanged Hook] path="/tmp/test.txt""#;
+ let data = parse_file_changed_hook(line);
+ assert_eq!(data.file, "/tmp/test.txt");
+ }
+
+ #[test]
+ fn test_parse_file_changed_hook_bare_path_fallback() {
+ let line = "[FileChanged Hook] /home/naomi/code/project/README.md";
+ let data = parse_file_changed_hook(line);
+ assert_eq!(data.file, "/home/naomi/code/project/README.md");
+ }
+
+ #[test]
+ fn test_parse_file_changed_hook_empty_line() {
+ let line = "[FileChanged Hook]";
+ let data = parse_file_changed_hook(line);
+ assert_eq!(data.file, "");
+ }
+
+ #[test]
+ fn test_parse_task_created_hook_with_all_fields() {
+ let line = r#"[TaskCreated Hook] task_id=Some("task-abc123"), description="Explore the codebase", parent_tool_use_id=Some("toolu_xyz")"#;
+ let data = parse_task_created_hook(line);
+ assert_eq!(data.task_id, Some("task-abc123".to_string()));
+ assert_eq!(data.description, Some("Explore the codebase".to_string()));
+ assert_eq!(data.parent_tool_use_id, Some("toolu_xyz".to_string()));
+ }
+
+ #[test]
+ fn test_parse_task_created_hook_with_description_only() {
+ let line = r#"[TaskCreated Hook] description="Search for relevant files""#;
+ let data = parse_task_created_hook(line);
+ assert_eq!(data.description, Some("Search for relevant files".to_string()));
+ assert_eq!(data.task_id, None);
+ assert_eq!(data.parent_tool_use_id, None);
+ }
+
+ #[test]
+ fn test_parse_task_created_hook_no_parent() {
+ let line = r#"[TaskCreated Hook] task_id=Some("task-001"), description="Run tests""#;
+ let data = parse_task_created_hook(line);
+ assert_eq!(data.task_id, Some("task-001".to_string()));
+ assert_eq!(data.description, Some("Run tests".to_string()));
+ assert_eq!(data.parent_tool_use_id, None);
+ }
+
+ #[test]
+ fn test_parse_task_created_hook_empty_line() {
+ let line = "[TaskCreated Hook]";
+ let data = parse_task_created_hook(line);
+ assert_eq!(data.task_id, None);
+ assert_eq!(data.description, None);
+ assert_eq!(data.parent_tool_use_id, None);
+ }
+
+ #[test]
+ fn test_parse_permission_denied_hook_with_all_fields() {
+ let line = r#"[PermissionDenied Hook] tool_name="Bash", reason="Tool not in allow list""#;
+ let data = parse_permission_denied_hook(line);
+ assert_eq!(data.tool_name, Some("Bash".to_string()));
+ assert_eq!(data.reason, Some("Tool not in allow list".to_string()));
+ }
+
+ #[test]
+ fn test_parse_permission_denied_hook_tool_only() {
+ let line = r#"[PermissionDenied Hook] tool_name="Edit""#;
+ let data = parse_permission_denied_hook(line);
+ assert_eq!(data.tool_name, Some("Edit".to_string()));
+ assert_eq!(data.reason, None);
+ }
+
+ #[test]
+ fn test_parse_permission_denied_hook_tool_key_alias() {
+ let line = r#"[PermissionDenied Hook] tool="Write", reason="Workspace not trusted""#;
+ let data = parse_permission_denied_hook(line);
+ assert_eq!(data.tool_name, Some("Write".to_string()));
+ assert_eq!(data.reason, Some("Workspace not trusted".to_string()));
+ }
+
+ #[test]
+ fn test_parse_permission_denied_hook_empty_line() {
+ let line = "[PermissionDenied Hook]";
+ let data = parse_permission_denied_hook(line);
+ assert_eq!(data.tool_name, None);
+ assert_eq!(data.reason, None);
+ }
+
#[test]
fn test_build_stop_failure_message_no_fields() {
let data = StopFailureData {
diff --git a/src/lib/components/CliVersion.svelte b/src/lib/components/CliVersion.svelte
index b48166f..a3120ee 100644
--- a/src/lib/components/CliVersion.svelte
+++ b/src/lib/components/CliVersion.svelte
@@ -2,7 +2,7 @@
import { invoke } from "@tauri-apps/api/core";
import { onMount } from "svelte";
- const SUPPORTED_CLI_VERSION = "2.1.80";
+ const SUPPORTED_CLI_VERSION = "2.1.104";
let installedVersion = $state("Loading...");
let latestNpmVersion = $state(null);
diff --git a/src/lib/components/ConfigSidebar.svelte b/src/lib/components/ConfigSidebar.svelte
index 0a26a2b..b9ccb1b 100644
--- a/src/lib/components/ConfigSidebar.svelte
+++ b/src/lib/components/ConfigSidebar.svelte
@@ -63,6 +63,7 @@
enable_claudeai_mcp_servers: true,
auto_memory_directory: null,
model_overrides: null,
+ disable_skill_shell_execution: false,
max_output_tokens: null,
trusted_workspaces: [],
background_image_path: null,
@@ -585,6 +586,22 @@
+
+
+
+
+ Passes disableSkillShellExecution: true to prevent skill scripts
+ from executing shell commands (requires Claude Code v2.1.91+)
+
+
+