Skip to content

Commit 1273d85

Browse files
halfaipgclaude
andcommitted
feat: Windows/PowerShell support, enriched system prompt, better error recovery
Shell execution: - Detect platform at runtime: PowerShell (pwsh/powershell) on Windows, $SHELL on Unix, fallback to /bin/sh - Shell tool description dynamically matches the platform - Windows-specific dangerous command patterns (format, del /f, rd /s, etc.) - Windows commands added to read-only permission whitelist - Windows write patterns added to subagent safety filter System prompt: - Inject OS/arch, shell name, git branch into environment section - Platform-aware shell guidance (PowerShell syntax on Windows) - Explicit error recovery instructions for edit_file failures - "ALWAYS read before editing" and "never repeat failed calls" rules Permission box: - Centered and pinned above input (not in scrollable viewport) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 84ac4a9 commit 1273d85

4 files changed

Lines changed: 167 additions & 33 deletions

File tree

agent.go

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"encoding/json"
66
"fmt"
77
"os"
8+
"os/exec"
89
"path/filepath"
10+
"runtime"
911
"strings"
1012
"sync"
1113
"time"
@@ -61,13 +63,13 @@ debug, and modify software projects.
6163
Available tools:
6264
- read_file: Read file contents with line numbers. Use offset/limit for large files.
6365
- write_file: Create or overwrite a file. Parent directories are created automatically.
64-
- edit_file: Surgical find-and-replace in a file. old_text must match exactly and be unique.
66+
- edit_file: Surgical find-and-replace in a file. old_text must match exactly and be unique. If old_text is not found, re-read the file and check for exact whitespace and line endings.
6567
- multi_edit: Batch multiple edits across files. Per-file atomicity with rollback.
6668
- list_files: List directory contents or glob for files (e.g. "**/*.go").
6769
- search_files: Regex search across files (powered by ripgrep). Find definitions, usages, etc.
6870
- web_search: Search the web. Use for current info, docs, versions, error solutions, or anything not in local files.
6971
- dispatch_agent: Spawn a read-only research subagent to investigate questions in isolated context.
70-
- shell: Run any shell command. Use for builds, tests, package management.
72+
- shell: Run a shell command. Use for builds, tests, package management, and any terminal task.
7173
- git_status: Show working tree status (staged, unstaged, untracked files).
7274
- git_diff: Show file diffs (staged, unstaged, or between refs).
7375
- git_log: Show recent commit history.
@@ -79,16 +81,21 @@ Available tools:
7981
- get_task: Get full details of a specific task.
8082
8183
Guidelines:
84+
- ALWAYS read a file before editing it — never guess at file contents
85+
- ALWAYS use search_files or list_files to explore before assuming project structure
8286
- You can call multiple tools in parallel — read_file, list_files, search_files, web_search, dispatch_agent, list_tasks, and get_task all run concurrently
83-
- Use list_files and search_files to explore the project before making changes
84-
- Read files before editing them — understand existing code first
8587
- Make targeted, minimal changes — don't rewrite entire files unnecessarily
8688
- For multiple related edits, prefer multi_edit over separate edit_file calls
8789
- Use git tools instead of shell for git operations — they provide structured output
88-
- After you edit files, the system may report diagnostics (errors, warnings) from language tools. If diagnostics appear, fix the issues before moving on.
89-
- If a tool fails, read the error and try a different approach
90+
- After you edit files, the system may report diagnostics (errors, warnings) from language tools. If diagnostics appear, fix the issues before moving on
9091
- When finished, briefly summarize what you changed and why
9192
93+
Error recovery:
94+
- If edit_file fails with "old_text not found", re-read the file to see the actual content, then retry with corrected text
95+
- If edit_file fails with "found N times", add more surrounding context lines to old_text to make it unique
96+
- If a shell command fails, read the error output carefully and try a different approach
97+
- Never repeat a failed tool call with identical arguments
98+
9299
Task management:
93100
- For multi-step work (3+ steps), create tasks upfront so the user can track progress
94101
- Set status to "in_progress" BEFORE starting work on a task
@@ -133,8 +140,23 @@ func strPtr(s string) *string { return &s }
133140
func buildSystemPrompt(workDir string) string {
134141
var sb strings.Builder
135142
sb.WriteString(systemPrompt)
136-
sb.WriteString(fmt.Sprintf("\n\nCurrent date: %s\n", time.Now().Format("2006-01-02")))
137-
sb.WriteString(fmt.Sprintf("Working directory: %s\n", workDir))
143+
144+
// Environment context
145+
sb.WriteString("\n\n## Environment\n\n")
146+
sb.WriteString(fmt.Sprintf("- Platform: %s/%s\n", runtime.GOOS, runtime.GOARCH))
147+
sb.WriteString(fmt.Sprintf("- Shell: %s\n", detectShellName()))
148+
sb.WriteString(fmt.Sprintf("- Date: %s\n", time.Now().Format("2006-01-02")))
149+
sb.WriteString(fmt.Sprintf("- Working directory: %s\n", workDir))
150+
151+
// Git context
152+
if branch := getGitBranch(workDir); branch != "" {
153+
sb.WriteString(fmt.Sprintf("- Git branch: %s\n", branch))
154+
}
155+
156+
// Platform-specific shell guidance
157+
if runtime.GOOS == "windows" {
158+
sb.WriteString("\nShell commands run in PowerShell. Use PowerShell syntax (e.g. `Get-ChildItem` not `ls`, `Remove-Item` not `rm`, `;` or `&&` to chain commands).\n")
159+
}
138160

139161
// Load project instructions if available
140162
projectInstructions := loadProjectInstructions(workDir)
@@ -155,6 +177,35 @@ func buildSystemPrompt(workDir string) string {
155177
return sb.String()
156178
}
157179

180+
// detectShellName returns the name of the shell that will be used for commands.
181+
func detectShellName() string {
182+
if runtime.GOOS == "windows" {
183+
if _, err := exec.LookPath("pwsh"); err == nil {
184+
return "pwsh (PowerShell Core)"
185+
}
186+
if _, err := exec.LookPath("powershell"); err == nil {
187+
return "powershell (Windows PowerShell)"
188+
}
189+
return "cmd.exe"
190+
}
191+
shell := os.Getenv("SHELL")
192+
if shell == "" {
193+
return "/bin/sh"
194+
}
195+
return filepath.Base(shell)
196+
}
197+
198+
// getGitBranch returns the current git branch name, or empty if not in a git repo.
199+
func getGitBranch(workDir string) string {
200+
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
201+
cmd.Dir = workDir
202+
out, err := cmd.Output()
203+
if err != nil {
204+
return ""
205+
}
206+
return strings.TrimSpace(string(out))
207+
}
208+
158209
// loadProjectInstructions looks for project config files (AGENTS.md, CLAUDE.md,
159210
// CODEX.md, .codebase) in the working directory and parent directories up to git root.
160211
func loadProjectInstructions(workDir string) string {

permission.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,30 @@ func shellNeedsPermission(cmd string) bool {
6161
cmdLower := strings.ToLower(strings.TrimSpace(cmd))
6262

6363
readOnlyPrefixes := []string{
64+
// Unix
6465
"ls", "cat ", "head ", "tail ", "grep ", "rg ", "find ",
6566
"wc ", "file ", "which ", "echo ", "pwd", "env", "printenv",
67+
"du ", "df ", "stat ", "date", "uname",
68+
"jq ", "sort ", "uniq ", "tr ",
69+
// Windows / PowerShell
70+
"dir ", "dir", "type ", "where ", "where.exe",
71+
"get-content ", "get-childitem ", "get-item ",
72+
"get-location", "get-process",
73+
"select-string ", "measure-object",
74+
"test-path ", "resolve-path",
75+
"systeminfo", "hostname", "whoami",
76+
"write-host ", "write-output ",
77+
// Git (cross-platform)
6678
"git status", "git log", "git diff", "git show", "git branch",
6779
"git remote", "git tag", "git stash list",
80+
// Build/test (cross-platform)
6881
"go vet", "go build", "go test", "go run",
69-
"tsc ", "npx tsc", "npm test", "npm run", "npm ls",
70-
"pytest", "python -c", "python3 -c",
82+
"tsc ", "npx tsc", "npx ", "npm test", "npm run", "npm ls",
83+
"pytest", "python -c", "python3 -c", "python -m pytest",
7184
"cargo check", "cargo test", "cargo build",
7285
"make ", "make -n", "make check",
73-
"du ", "df ", "stat ", "date", "uname",
7486
"node -e", "node --eval",
75-
"jq ", "sort ", "uniq ", "tr ",
87+
"dotnet build", "dotnet test",
7688
}
7789

7890
for _, prefix := range readOnlyPrefixes {

subagent.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"runtime"
78
"strings"
89
)
910

@@ -18,7 +19,7 @@ import (
1819

1920
const subagentMaxTurns = 25
2021

21-
const subagentSystemPrompt = `You are a focused research assistant. You help gather information by reading files, searching code, and listing directories.
22+
const subagentSystemPromptBase = `You are a focused research assistant. You help gather information by reading files, searching code, and listing directories.
2223
2324
You have read-only access to the project. You CANNOT modify any files.
2425
@@ -29,7 +30,7 @@ Guidelines:
2930
- Use search_files to find relevant code quickly
3031
- Use list_files to explore project structure
3132
- Use web_search when you need external documentation, API references, or current information
32-
- Use shell for read-only commands only (ls, cat, grep, git log, etc.)
33+
- Use shell for read-only commands only (e.g. git log, git status, directory listings)
3334
- When done, provide a clear, concise summary of your findings`
3435

3536
// subagentToolDefs contains only read-only tools.
@@ -47,7 +48,7 @@ func init() {
4748

4849
// RunSubagent executes a read-only research subagent and returns its final text.
4950
func RunSubagent(client *LLMClient, workDir, task string) (string, error) {
50-
sysContent := subagentSystemPrompt + fmt.Sprintf("\n\nWorking directory: %s", workDir)
51+
sysContent := subagentSystemPromptBase + fmt.Sprintf("\n\nPlatform: %s\nWorking directory: %s", runtime.GOOS, workDir)
5152

5253
history := []ChatMessage{
5354
{Role: "system", Content: strPtr(sysContent)},
@@ -145,14 +146,24 @@ func RunSubagent(client *LLMClient, workDir, task string) (string, error) {
145146

146147
// shellWritePatterns detects commands that modify the filesystem.
147148
var shellWritePatterns = []string{
149+
// Unix
148150
"rm ", "rm\t", "rmdir", "mv ", "mv\t", "cp ", "cp\t",
149151
"mkdir", "touch ", "chmod", "chown",
150152
"tee ", "tee\t", "truncate",
153+
"sed -i", "patch ",
154+
// Windows / PowerShell
155+
"del ", "del\t", "erase ", "rd ", "rd\t",
156+
"move ", "move\t", "copy ", "copy\t", "xcopy",
157+
"ren ", "rename ", "md ", "md\t",
158+
"remove-item", "move-item", "copy-item",
159+
"new-item", "set-content", "add-content",
160+
"out-file", "invoke-webrequest",
161+
// Git (cross-platform)
151162
"git checkout", "git reset", "git clean", "git stash",
152163
"git merge", "git rebase", "git commit", "git push",
164+
// Package managers (cross-platform)
153165
"npm install", "yarn add", "pip install",
154166
"go install", "go get",
155-
"sed -i", "patch ",
156167
}
157168

158169
// executeReadOnlyShell runs a shell command but blocks write-like commands.

tools.go

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/exec"
88
"path/filepath"
9+
"runtime"
910
"sort"
1011
"strings"
1112
"time"
@@ -338,17 +339,14 @@ var toolDefs = []ToolDef{
338339
{
339340
Type: "function",
340341
Function: ToolDefFunction{
341-
Name: "shell",
342-
Description: "Execute a shell command in the project directory. " +
343-
"Use for: running builds, tests, installing packages, git commands, and any terminal task. " +
344-
"Commands run in a bash shell. The full stdout + stderr is returned. " +
345-
"Long-running commands are killed after the timeout.",
342+
Name: "shell",
343+
Description: shellToolDescription(),
346344
Parameters: map[string]interface{}{
347345
"type": "object",
348346
"properties": map[string]interface{}{
349347
"command": map[string]interface{}{
350348
"type": "string",
351-
"description": "The shell command to execute. Use && to chain commands.",
349+
"description": shellCommandDescription(),
352350
},
353351
},
354352
"required": []string{"command"},
@@ -1139,17 +1137,79 @@ func toolWebSearch(args map[string]interface{}) (string, bool) {
11391137

11401138
// ── shell ────────────────────────────────────────────────────
11411139

1140+
// shellToolDescription returns a platform-aware description for the shell tool.
1141+
func shellToolDescription() string {
1142+
if runtime.GOOS == "windows" {
1143+
return "Execute a shell command in the project directory. " +
1144+
"Use for: running builds, tests, installing packages, git commands, and any terminal task. " +
1145+
"Commands run in PowerShell on Windows. The full stdout + stderr is returned. " +
1146+
"Long-running commands are killed after the timeout."
1147+
}
1148+
return "Execute a shell command in the project directory. " +
1149+
"Use for: running builds, tests, installing packages, git commands, and any terminal task. " +
1150+
"Commands run in a bash shell. The full stdout + stderr is returned. " +
1151+
"Long-running commands are killed after the timeout."
1152+
}
1153+
1154+
// shellCommandDescription returns platform-aware help for the command parameter.
1155+
func shellCommandDescription() string {
1156+
if runtime.GOOS == "windows" {
1157+
return "The shell command to execute. Use ; or && to chain commands. Use PowerShell syntax."
1158+
}
1159+
return "The shell command to execute. Use && to chain commands."
1160+
}
1161+
11421162
// dangerousPatterns detects potentially destructive shell commands.
1143-
var dangerousPatterns = []string{
1144-
"rm -rf /",
1145-
"rm -rf ~",
1146-
"rm -rf $HOME",
1147-
":(){:|:&};:", // fork bomb
1148-
"mkfs.",
1149-
"dd if=",
1150-
"> /dev/sd",
1151-
"chmod -R 777 /",
1152-
":(){ :|:& };:", // fork bomb variant
1163+
var dangerousPatterns = func() []string {
1164+
base := []string{
1165+
"rm -rf /",
1166+
"rm -rf ~",
1167+
"rm -rf $HOME",
1168+
":(){:|:&};:", // fork bomb
1169+
"mkfs.",
1170+
"dd if=",
1171+
"> /dev/sd",
1172+
"chmod -R 777 /",
1173+
":(){ :|:& };:", // fork bomb variant
1174+
}
1175+
if runtime.GOOS == "windows" {
1176+
base = append(base,
1177+
"format c:",
1178+
"format d:",
1179+
"del /f /s /q c:\\",
1180+
"rd /s /q c:\\",
1181+
"rmdir /s /q c:\\",
1182+
"remove-item -recurse -force c:\\",
1183+
"remove-item -recurse -force $env:",
1184+
)
1185+
}
1186+
return base
1187+
}()
1188+
1189+
// getShellCommand returns the appropriate shell binary and args for the platform.
1190+
func getShellCommand(command string) *exec.Cmd {
1191+
if runtime.GOOS == "windows" {
1192+
// Try PowerShell first (pwsh = PowerShell Core, powershell = Windows PowerShell)
1193+
if _, err := exec.LookPath("pwsh"); err == nil {
1194+
return exec.Command("pwsh", "-NoProfile", "-NonInteractive", "-Command", command)
1195+
}
1196+
if _, err := exec.LookPath("powershell"); err == nil {
1197+
return exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", command)
1198+
}
1199+
// Fallback to cmd.exe
1200+
shell := os.Getenv("COMSPEC")
1201+
if shell == "" {
1202+
shell = "cmd.exe"
1203+
}
1204+
return exec.Command(shell, "/C", command)
1205+
}
1206+
1207+
// Unix: use SHELL env var, fallback to /bin/sh
1208+
shell := os.Getenv("SHELL")
1209+
if shell == "" {
1210+
shell = "/bin/sh"
1211+
}
1212+
return exec.Command(shell, "-c", command)
11531213
}
11541214

11551215
func toolShell(args map[string]interface{}, workDir string) (string, bool) {
@@ -1166,7 +1226,7 @@ func toolShell(args map[string]interface{}, workDir string) (string, bool) {
11661226
}
11671227
}
11681228

1169-
cmd := exec.Command("bash", "-c", command)
1229+
cmd := getShellCommand(command)
11701230
cmd.Dir = workDir
11711231
cmd.Env = append(os.Environ(),
11721232
"TERM=dumb",

0 commit comments

Comments
 (0)