Restrict Claude Code tools and require approval for unknown tools (#637)

Claude Code chats get no built-in tools and builders an explicit list (commands, files, search, todos), so tools that message other sessions, publish or schedule work never start; in Auto mode a tool without an approval rule is refused (fixes #635).
This commit is contained in:
Max Mittelstädt
2026-11-03 07:01:45 -08:00
committed by GitHub
parent f05f019691
commit 2920a7c40f
3 changed files with 19 additions and 1 deletions
+2
View File
@@ -290,6 +290,8 @@ impl AgentAdapter for ClaudeCode {
if let Some(r) = cx.resume {
a.extend(["--resume".into(), r.to_string()]);
}
let tools = if cx.builder { "Bash,PowerShell,Read,Write,Edit,MultiEdit,Glob,Grep,TodoWrite,NotebookEdit" } else { "" };
a.extend(["--tools".into(), tools.into()]);
if cx.builder {
// Questions go through our core_person_ask tool, which the app shows as buttons.
a.extend(["--permission-prompt-tool".into(), format!("mcp__{MCP_NAME}__{APPROVAL_TOOL}"), "--disallowedTools".into(), "AskUserQuestion".into()]);
+8
View File
@@ -1146,6 +1146,14 @@ mod tests {
let pos = |f: &str| a.iter().position(|x| x == f).unwrap();
assert_eq!(a[pos("--output-format") + 1], "stream-json");
assert!(a.contains(&"--verbose".to_string()));
assert_eq!(a[pos("--tools") + 1], "", "built-in tools are opt-in");
for access in [Access::Build, Access::CleanRoomBuild] {
let builder = build_args(adapter_for("claude-code").as_ref(), &BackendOptions::default(), "build it", "PRE", &mcp(), None, access);
let builder_pos = builder.iter().position(|x| x == "--tools").unwrap();
let tools = &builder[builder_pos + 1];
assert!(tools.split(',').any(|tool| tool == "Bash"), "{access:?}: {tools}");
assert!(tools.split(',').any(|tool| tool == "Write"), "{access:?}: {tools}");
}
assert_eq!(a[pos("--resume") + 1], "sess-1");
assert_eq!(a[pos("--allowedTools") + 1], "mcp__craft-anything__*");
assert_eq!(a[pos("--model") + 1], "claude-opus-5-5", "Opus 5.5 unless configured otherwise");
+9 -1
View File
@@ -539,7 +539,7 @@ pub fn never_unasked(ws: Option<&Workspace>, tool: &str, input: &Value) -> Optio
}
None
}
_ => None,
_ => refuse("uses a tool that has no automatic approval rule"),
}
}
@@ -991,6 +991,14 @@ mod tests {
assert_eq!((back["questions"].as_str(), back["chosen"].is_null(), back["app_default"].as_str()), (Some("ask"), true, Some("ask")));
}
#[test]
fn auto_mode_refuses_unknown_tools_that_may_publish_or_schedule() {
for tool in ["Artifact", "CronCreate", "ListAgents"] {
let result = never_unasked(None, tool, &json!({}));
assert!(result.is_some(), "{tool} must require the person's approval");
}
}
#[test]
fn auto_mode_keeps_going_but_never_does_the_dangerous_things() {
let (_d, w) = ws();