@@ -106,7 +106,7 @@ func (t *readFileTool) Call(argsJSON string) (string, error) {
106106 return jsonError (fmt .Sprintf ("%q is a directory, not a file" , args .Path ))
107107 }
108108
109- // Check for binary content — only examine first 8KB
109+ // Single pass: open → binary check from sample → seek → read+count
110110 f , err := os .Open (args .Path )
111111 if err != nil {
112112 return jsonError (fmt .Sprintf ("cannot open %q: %v" , args .Path , err ))
@@ -115,17 +115,16 @@ func (t *readFileTool) Call(argsJSON string) (string, error) {
115115
116116 sample := make ([]byte , 8192 )
117117 n , _ := f .Read (sample )
118- sample = sample [:n ]
119- if isBinary (sample ) {
118+ if isBinary (sample [:n ]) {
120119 return jsonError (fmt .Sprintf ("%q appears to be a binary file — use shell to read it" , args .Path ))
121120 }
122- f .Close () // close to re-read from start
123121
124- // Count total lines first
125- totalLines := countLines (args .Path )
122+ // Seek back to start for the full scan
123+ if _ , err := f .Seek (0 , 0 ); err != nil {
124+ return jsonError (fmt .Sprintf ("cannot seek %q: %v" , args .Path , err ))
125+ }
126126
127- // Read the requested range
128- content , err := readLines (args .Path , args .Offset , args .Limit )
127+ content , totalLines , err := readLinesWithCount (f , args .Offset , args .Limit )
129128 if err != nil {
130129 return jsonError (fmt .Sprintf ("cannot read %q: %v" , args .Path , err ))
131130 }
@@ -205,9 +204,31 @@ func (t *writeFileTool) Call(argsJSON string) (string, error) {
205204 }
206205 }
207206
208- if err := os .WriteFile (args .Path , []byte (args .Content ), 0644 ); err != nil {
207+ // Atomic write via temp file + rename to prevent TOCTOU symlink races.
208+ // os.CreateTemp creates the file in the same directory (same filesystem),
209+ // and os.Rename atomically replaces the directory entry without following
210+ // symlinks — closing the window between check and write.
211+ tmpFile , err := os .CreateTemp (dir , ".tmp_write_*" )
212+ if err != nil {
213+ return jsonError (fmt .Sprintf ("cannot create temp file: %v" , err ))
214+ }
215+ tmpPath := tmpFile .Name ()
216+
217+ if _ , err := tmpFile .Write ([]byte (args .Content )); err != nil {
218+ tmpFile .Close ()
219+ os .Remove (tmpPath )
209220 return jsonError (fmt .Sprintf ("cannot write %q: %v" , args .Path , err ))
210221 }
222+ if err := tmpFile .Close (); err != nil {
223+ os .Remove (tmpPath )
224+ return jsonError (fmt .Sprintf ("cannot close temp file: %v" , err ))
225+ }
226+
227+ // Atomic rename — replaces target directory entry without following symlinks
228+ if err := os .Rename (tmpPath , args .Path ); err != nil {
229+ os .Remove (tmpPath )
230+ return jsonError (fmt .Sprintf ("cannot rename %q: %v" , args .Path , err ))
231+ }
211232
212233 return jsonResult (writeFileResult {
213234 Success : true ,
@@ -346,24 +367,25 @@ func (t *searchFilesTool) searchContent(args searchFilesArgs) (string, error) {
346367 }
347368 }
348369
349- // Skip binary files
350- sample := make ([]byte , 512 )
370+ // Skip binary files — single open for check then search
351371 f , err := os .Open (path )
352372 if err != nil {
353373 return nil
354374 }
375+ defer f .Close ()
376+
377+ sample := make ([]byte , 512 )
355378 n , _ := f .Read (sample )
356- f .Close ()
357379 if isBinary (sample [:n ]) {
358380 return nil
359381 }
360382
361- // Search line by line
362- f , err = os .Open (path )
363- if err != nil {
383+ // Seek back to start for content search
384+ if _ , err := f .Seek (0 , 0 ); err != nil {
364385 return nil
365386 }
366- defer f .Close ()
387+
388+ // Search line by line
367389
368390 scanner := bufio .NewScanner (f )
369391 scanner .Buffer (make ([]byte , 1024 * 1024 ), 1024 * 1024 )
@@ -601,28 +623,9 @@ func isBinary(data []byte) bool {
601623 return float64 (nonPrintable )/ float64 (len (data )) > 0.30
602624}
603625
604- func countLines (path string ) int {
605- f , err := os .Open (path )
606- if err != nil {
607- return 0
608- }
609- defer f .Close ()
610- count := 0
611- scanner := bufio .NewScanner (f )
612- scanner .Buffer (make ([]byte , 1024 * 1024 ), 1024 * 1024 )
613- for scanner .Scan () {
614- count ++
615- }
616- return count
617- }
618-
619- func readLines (path string , offset , limit int ) (string , error ) {
620- f , err := os .Open (path )
621- if err != nil {
622- return "" , err
623- }
624- defer f .Close ()
625-
626+ // readLinesWithCount reads lines from an open file, returning content
627+ // and total line count in a single pass. offset is 1-based, limit caps lines.
628+ func readLinesWithCount (f * os.File , offset , limit int ) (string , int , error ) {
626629 var out strings.Builder
627630 scanner := bufio .NewScanner (f )
628631 scanner .Buffer (make ([]byte , 1024 * 1024 ), 1024 * 1024 )
@@ -636,12 +639,19 @@ func readLines(path string, offset, limit int) (string, error) {
636639 continue
637640 }
638641 if lineNum > end {
639- break
642+ continue // count total even beyond limit
640643 }
641644 out .WriteString (fmt .Sprintf ("%d|%s\n " , lineNum , scanner .Text ()))
642645 }
643646
644- return strings .TrimSuffix (out .String (), "\n " ), scanner .Err ()
647+ // If no limit was set (limit=0), continue counting past start
648+ if limit > 0 {
649+ for scanner .Scan () {
650+ lineNum ++
651+ }
652+ }
653+
654+ return strings .TrimSuffix (out .String (), "\n " ), lineNum , scanner .Err ()
645655}
646656
647657func truncateDiff (s string , maxLen int ) string {
0 commit comments