Skip to content

WorkingDirectory session parameter not applied to built-in tools when SystemMessage.Mode is "replace" #408

@sdehm

Description

@sdehm

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

  1. 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.")
	}
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions