From 0e4a970e3adc6da92b0dd8f4ebd8e6e148024bf7 Mon Sep 17 00:00:00 2001 From: dave Date: Thu, 30 Apr 2026 13:44:51 +0000 Subject: [PATCH] fix(883): canonical Bash(:*) syntax in scaffold settings template Claude Code 2.1.123+ honours wildcard Bash allowlist patterns only in the canonical form `Bash(cmd:*)`. The space form `Bash(cmd *)` falls through to prompt_permission and gets auto-denied in agent mode, breaking spawned coders. - Rewrite all `Bash(cmd *)` patterns in STORY_KIT_CLAUDE_SETTINGS to the colon form. - Replace separate `Bash(cargo build:*)` / `Bash(cargo check:*)` with a single `Bash(cargo:*)`. - Add commonly-needed patterns: python3, node, npm, which, sed, awk, rg, diff, sort, uniq. - Patch the live project-root .claude/settings.json so the running system picks up the fix immediately (rebuilt scaffolds will match). - Add regression test asserting no `Bash(... *)` patterns survive and required common commands are present. --- .claude/settings.json | 44 ++++++++++++++++---------- server/src/io/fs/scaffold/templates.rs | 37 ++++++++++++++-------- server/src/io/fs/scaffold/tests.rs | 43 +++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 31 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index fc28af23..da240ff4 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,28 +1,38 @@ { "permissions": { "allow": [ - "Bash(cargo build:*)", - "Bash(cargo check:*)", - "Bash(git *)", - "Bash(ls *)", - "Bash(mkdir *)", - "Bash(mv *)", - "Bash(rm *)", - "Bash(touch *)", + "Bash(cargo:*)", + "Bash(git:*)", + "Bash(ls:*)", + "Bash(mkdir:*)", + "Bash(mv:*)", + "Bash(rm:*)", + "Bash(touch:*)", "Bash(echo:*)", - "Bash(pwd *)", + "Bash(pwd:*)", "Bash(grep:*)", - "Bash(find *)", - "Bash(head *)", - "Bash(tail *)", - "Bash(wc *)", - "Bash(cat *)", + "Bash(find:*)", + "Bash(head:*)", + "Bash(tail:*)", + "Bash(wc:*)", + "Bash(cat:*)", + "Bash(python3:*)", + "Bash(node:*)", + "Bash(npm:*)", + "Bash(which:*)", + "Bash(sed:*)", + "Bash(awk:*)", + "Bash(sort:*)", + "Bash(uniq:*)", + "Bash(diff:*)", + "Bash(rg:*)", + "Read", "Edit", "Write", + "Glob", + "Grep", "mcp__huskies__*" ] }, - "enabledMcpjsonServers": [ - "huskies" - ] + "enabledMcpjsonServers": ["huskies"] } diff --git a/server/src/io/fs/scaffold/templates.rs b/server/src/io/fs/scaffold/templates.rs index 90f61112..b21d316e 100644 --- a/server/src/io/fs/scaffold/templates.rs +++ b/server/src/io/fs/scaffold/templates.rs @@ -70,22 +70,31 @@ setup wizard instructions and guide the user through it conversationally.\n"; pub(super) const STORY_KIT_CLAUDE_SETTINGS: &str = r#"{ "permissions": { "allow": [ - "Bash(cargo build:*)", - "Bash(cargo check:*)", - "Bash(git *)", - "Bash(ls *)", - "Bash(mkdir *)", - "Bash(mv *)", - "Bash(rm *)", - "Bash(touch *)", + "Bash(cargo:*)", + "Bash(git:*)", + "Bash(ls:*)", + "Bash(mkdir:*)", + "Bash(mv:*)", + "Bash(rm:*)", + "Bash(touch:*)", "Bash(echo:*)", - "Bash(pwd *)", + "Bash(pwd:*)", "Bash(grep:*)", - "Bash(find *)", - "Bash(head *)", - "Bash(tail *)", - "Bash(wc *)", - "Bash(cat *)", + "Bash(find:*)", + "Bash(head:*)", + "Bash(tail:*)", + "Bash(wc:*)", + "Bash(cat:*)", + "Bash(python3:*)", + "Bash(node:*)", + "Bash(npm:*)", + "Bash(which:*)", + "Bash(sed:*)", + "Bash(awk:*)", + "Bash(rg:*)", + "Bash(diff:*)", + "Bash(sort:*)", + "Bash(uniq:*)", "Read", "Edit", "Write", diff --git a/server/src/io/fs/scaffold/tests.rs b/server/src/io/fs/scaffold/tests.rs index 39df273c..9fd8797d 100644 --- a/server/src/io/fs/scaffold/tests.rs +++ b/server/src/io/fs/scaffold/tests.rs @@ -592,3 +592,46 @@ fn scaffold_does_not_overwrite_existing_project_toml_with_components() { "scaffold should not overwrite existing project.toml" ); } + +#[test] +fn scaffold_story_kit_claude_settings_uses_canonical_bash_syntax() { + // Regression for bug 883. Claude Code 2.1.123+ honours wildcard Bash + // allowlist patterns only in the canonical form `Bash(cmd:*)`. The space + // form `Bash(cmd *)` falls through to prompt_permission and gets + // auto-denied in agent mode, breaking spawned coders. + let dir = tempdir().unwrap(); + scaffold_story_kit(dir.path(), 3001).unwrap(); + + let settings = fs::read_to_string(dir.path().join(".claude/settings.json")).unwrap(); + + // No `Bash(... *)` (space before asterisk) patterns may survive. + let space_form = regex::Regex::new(r#""Bash\([^"]* \*\)""#).unwrap(); + if let Some(m) = space_form.find(&settings) { + panic!( + "settings.json contains non-canonical Bash pattern: {} \ + (use `Bash(cmd:*)` form instead)", + m.as_str() + ); + } + + // Common safe commands must be allowlisted in canonical form. + for required in &[ + r#""Bash(cargo:*)""#, + r#""Bash(git:*)""#, + r#""Bash(ls:*)""#, + r#""Bash(cat:*)""#, + r#""Bash(grep:*)""#, + r#""Bash(find:*)""#, + r#""Bash(python3:*)""#, + r#""Bash(node:*)""#, + r#""Bash(npm:*)""#, + r#""Bash(rg:*)""#, + r#""Bash(sed:*)""#, + r#""Bash(awk:*)""#, + ] { + assert!( + settings.contains(required), + "settings.json missing required allowlist pattern: {required}" + ); + } +}