@@ -6,6 +6,7 @@ import (
66 "fmt"
77 "os"
88 "path/filepath"
9+ "strings"
910
1011 "charm.land/fantasy"
1112 "github.com/charmbracelet/crush/internal/filepathext"
@@ -57,17 +58,50 @@ func NewTouchTool(
5758
5859 filePath := filepathext.SmartJoin(workingDir, params.FilePath)
5960
60− fileInfo, err := os.Stat(filePath)
61+ absWorkingDir, err := filepath.Abs(workingDir)
62+ if err != nil {
63+ return fantasy.ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
64+ }
65+ absFilePath, err := filepath.Abs(filePath)
66+ if err != nil {
67+ return fantasy.ToolResponse{}, fmt.Errorf("error resolving file path: %w", err)
68+ }
69+ relPath, relErr := filepath.Rel(absWorkingDir, absFilePath)
70+ isOutsideWorkDir := relErr != nil || strings.HasPrefix(relPath, "..")
71+
72+ if isOutsideWorkDir {
73+ granted, permReqErr := permissions.Request(ctx,
74+ permission.CreatePermissionRequest{
75+ SessionID: sessionID,
76+ Path: absFilePath,
77+ ToolCallID: call.ID,
78+ ToolName: TouchToolName,
79+ Action: "write",
80+ Description: fmt.Sprintf("Create empty file outside working directory: %s", absFilePath),
81+ Params: TouchPermissionsParams{
82+ FilePath: absFilePath,
83+ },
84+ },
85+ )
86+ if permReqErr != nil {
87+ return fantasy.ToolResponse{}, permReqErr
88+ }
89+ if !granted {
90+ return NewPermissionDeniedResponse(), nil
91+ }
92+ }
93+
94+ fileInfo, err := os.Stat(absFilePath)
6195 if err == nil {
6296 if fileInfo.IsDir() {
63− return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
97+ return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", absFilePath)), nil
6498 }
65− return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", filePath)), nil
99+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", absFilePath)), nil
66100 } else if !os.IsNotExist(err) {
67101 return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
68102 }
69103
70− dir := filepath.Dir(filePath)
104+ dir := filepath.Dir(absFilePath)
71105 if err = os.MkdirAll(dir, 0o755); err != nil {
72106 return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
73107 }
@@ -75,13 +109,13 @@ func NewTouchTool(
75109 p, err := permissions.Request(ctx,
76110 permission.CreatePermissionRequest{
77111 SessionID: sessionID,
78− Path: fsext.PathOrPrefix(filePath, workingDir),
112+ Path: fsext.PathOrPrefix(absFilePath, absWorkingDir),
79113 ToolCallID: call.ID,
80114 ToolName: TouchToolName,
81115 Action: "write",
82− Description: fmt.Sprintf("Create empty file %s", filePath),
116+ Description: fmt.Sprintf("Create empty file %s", absFilePath),
83117 Params: TouchPermissionsParams{
84− FilePath: filePath,
118+ FilePath: absFilePath,
85119 OldContent: "",
86120 NewContent: "",
87121 },
@@ -94,10 +128,10 @@ func NewTouchTool(
94128 return NewPermissionDeniedResponse(), nil
95129 }
96130
97− file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644)
131+ file, err := os.OpenFile(absFilePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o644)
98132 if err != nil {
99133 if os.IsExist(err) {
100− return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", filePath)), nil
134+ return fantasy.NewTextErrorResponse(fmt.Sprintf("File already exists: %s", absFilePath)), nil
101135 }
102136 return fantasy.ToolResponse{}, fmt.Errorf("error creating file: %w", err)
103137 }
@@ -105,21 +139,21 @@ func NewTouchTool(
105139 return fantasy.ToolResponse{}, fmt.Errorf("error closing file: %w", err)
106140 }
107141
108− _, err = files.Create(ctx, sessionID, filePath, "")
142+ _, err = files.Create(ctx, sessionID, absFilePath, "")
109143 if err != nil {
110144 return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
111145 }
112146
113− filetracker.RecordRead(ctx, sessionID, filePath)
147+ filetracker.RecordRead(ctx, sessionID, absFilePath)
114148
115− notifyLSPs(ctx, lspManager, filePath)
149+ notifyLSPs(ctx, lspManager, absFilePath)
116150
117− result := fmt.Sprintf("Empty file successfully created: %s", filePath)
151+ result := fmt.Sprintf("Empty file successfully created: %s", absFilePath)
118152 result = fmt.Sprintf("<result>\n%s\n</result>", result)
119− result += getDiagnostics(filePath, lspManager)
153+ result += getDiagnostics(absFilePath, lspManager)
120154 return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
121155 TouchResponseMetadata{
122− FilePath: filePath,
156+ FilePath: absFilePath,
123157 },
124158 ), nil
125159 })