-
Notifications
You must be signed in to change notification settings - Fork 807
Description
SDK version: github.com/github/copilot-sdk/go v0.1.21
Copilot CLI v0.0.406
When creating a session with SessionConfig.WorkingDirectory set and SystemMessage.Mode set to "replace", the CLI's built-in tools (create, bash, etc.) ignore the configured working directory. Instead, the LLM attempts to write to /workspace or falls back to /tmp. The WorkingDirectory session parameter should ensure built-in tools resolve paths correctly regardless of system prompt configuration or this should be documented.
Example
Permission request logs show the CLI requesting paths like mkdir -p /workspace and fileName:/hello.txt instead of the configured working directory:
[PERMISSION] kind=shell commands:[{identifier:mkdir -p /workspace readOnly:false}]
[PERMISSION] kind=write fileName:/hello.txt ...
Setting WorkingDirectory on SessionConfig should be sufficient for built-in tools to resolve file paths correctly, even when the system prompt is replaced. I could imagine including the specified working directory in the tool descriptions themselves or perhaps there is another place that makes more sense but doesn't replace the flexibility of being able to replace the system prompt.
Workaround
- Include the working directory path in the replacement system prompt: Since Mode: "replace" strips the CLI's default {{cwd}} context, explicitly add the path back, e.g. "The current working directory is: /path/to/workspace".
This may be related to github/copilot-cli#884 but I don't think you can replace the system prompt in the cli without using custom agents.
Here is a minimal reproduction:
// Minimal reproduction: WorkingDirectory session parameter is not sufficient
// for built-in tools when SystemMessage Mode is "replace".
//
// When the system prompt is replaced, the CLI's built-in instructions about
// the working directory path are lost. The LLM then defaults to using
// "/workspace" (a hardcoded path from the CLI's default prompt) instead of
// the directory specified via SessionConfig.WorkingDirectory.
//
// Steps to reproduce:
// 1. go run main.go
// 2. Observe that no file appears in ./test_workspace/
// 3. Check stdout — the CLI tries "/workspace" instead of the real path
package main
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
sdk "github.com/github/copilot-sdk/go"
)
func main() {
// Create a workspace directory with an absolute path.
workDir, err := filepath.Abs("./test_workspace")
if err != nil {
log.Fatal(err)
}
os.RemoveAll(workDir)
if err := os.MkdirAll(workDir, 0755); err != nil {
log.Fatal(err)
}
fmt.Printf("WorkingDirectory set to: %s\n\n", workDir)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
client := sdk.NewClient(&sdk.ClientOptions{
LogLevel: "error",
AutoStart: sdk.Bool(true),
})
if err := client.Start(ctx); err != nil {
log.Fatalf("failed to start client: %v", err)
}
defer client.Stop()
// Create a session with:
// - WorkingDirectory set to our workspace (absolute path)
// - SystemMessage Mode "replace" (common for agent frameworks)
// - OnPermissionRequest approving all requests
session, err := client.CreateSession(ctx, &sdk.SessionConfig{
Model: "gpt-4.1",
WorkingDirectory: workDir,
SystemMessage: &sdk.SystemMessageConfig{
Mode: "replace",
Content: "You are a helpful assistant. " +
"Create files using the built-in create tool. " +
"All file paths should be relative to the working directory.",
},
OnPermissionRequest: func(req sdk.PermissionRequest, inv sdk.PermissionInvocation) (sdk.PermissionRequestResult, error) {
fmt.Printf("[PERMISSION] kind=%s extra=%v\n\n", req.Kind, req.Extra)
return sdk.PermissionRequestResult{Kind: "approved"}, nil
},
})
if err != nil {
log.Fatalf("failed to create session: %v", err)
}
done := make(chan struct{})
session.On(func(event sdk.SessionEvent) {
switch event.Type {
case sdk.AssistantMessageDelta:
if event.Data.DeltaContent != nil {
fmt.Print(*event.Data.DeltaContent)
}
case sdk.AssistantTurnEnd:
fmt.Println("\n--- TURN END ---")
close(done)
}
})
// Note: we do NOT include the absolute workspace path in the prompt.
// The LLM should derive it from the WorkingDirectory session parameter.
_, err = session.Send(ctx, sdk.MessageOptions{
Prompt: "Create a file called hello.txt containing 'Hello World'.",
})
if err != nil {
log.Fatalf("Send error: %v", err)
}
select {
case <-done:
case <-ctx.Done():
fmt.Println("Timed out")
}
// Check results.
expectedPath := filepath.Join(workDir, "hello.txt")
fmt.Println()
if _, err := os.Stat(expectedPath); err == nil {
content, _ := os.ReadFile(expectedPath)
fmt.Printf("✅ File created at %s: %s\n", expectedPath, string(content))
} else {
fmt.Printf("❌ File NOT created at %s\n", expectedPath)
fmt.Println()
fmt.Println("Bug: WorkingDirectory is set to an absolute path but the CLI's")
fmt.Println("built-in tools use '/workspace' (from the default system prompt")
fmt.Println("that was replaced). The WorkingDirectory session parameter should")
fmt.Println("be sufficient for built-in tools to resolve paths correctly,")
fmt.Println("regardless of whether the system prompt is replaced.")
}
}