← back to Cli Printing Press
fix(cli): create llm prompt temp files privately (#1674)
c5d86825c65118f377d3515e1ce0ef32750c447f · 2026-05-19 15:07:15 -0700 · Trevin Chow
Files touched
M internal/llm/llm.goM internal/llm/llm_test.go
Diff
commit c5d86825c65118f377d3515e1ce0ef32750c447f
Author: Trevin Chow <trevin@trevinchow.com>
Date: Tue May 19 15:07:15 2026 -0700
fix(cli): create llm prompt temp files privately (#1674)
---
internal/llm/llm.go | 38 ++++++++++++++++++--------
internal/llm/llm_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 97 insertions(+), 11 deletions(-)
diff --git a/internal/llm/llm.go b/internal/llm/llm.go
index 8ee50201..8fb6ebde 100644
--- a/internal/llm/llm.go
+++ b/internal/llm/llm.go
@@ -4,9 +4,7 @@ import (
"fmt"
"os"
"os/exec"
- "path/filepath"
"strings"
- "time"
)
// Available returns true if any supported LLM CLI is installed.
@@ -17,16 +15,9 @@ func Available() bool {
}
// Run sends a prompt to the best available LLM CLI and returns the response.
-// It tries claude first, then falls back to codex. The prompt is written to a
-// temp file and passed via the -p flag to avoid ARG_MAX issues with large prompts.
+// It tries claude first, then falls back to codex. Long Claude prompts are
+// written to a temp file and passed by reference to avoid ARG_MAX issues.
func Run(prompt string) (string, error) {
- // Write prompt to temp file to avoid ARG_MAX issues
- tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("llm-prompt-%d.md", time.Now().UnixNano()))
- if err := os.WriteFile(tmpFile, []byte(prompt), 0644); err != nil {
- return "", fmt.Errorf("writing prompt: %w", err)
- }
- defer func() { _ = os.Remove(tmpFile) }()
-
// Try claude first (-p / --print mode, prompt as positional arg)
if path, err := exec.LookPath("claude"); err == nil {
// For short prompts, pass directly. For long prompts, use a temp file referenced in the prompt.
@@ -34,6 +25,12 @@ func Run(prompt string) (string, error) {
if len(prompt) < 100000 {
cmd = exec.Command(path, "-p", prompt, "--output-format", "text")
} else {
+ tmpFile, cleanup, err := writePromptTempFile(prompt)
+ if err != nil {
+ return "", err
+ }
+ defer cleanup()
+
// Write to temp file and tell Claude to read it
metaPrompt := fmt.Sprintf("Read the file at %s and follow the instructions inside it exactly.", tmpFile)
cmd = exec.Command(path, "-p", metaPrompt, "--output-format", "text")
@@ -60,3 +57,22 @@ func Run(prompt string) (string, error) {
return "", fmt.Errorf("no LLM CLI found (install claude or codex)")
}
+
+func writePromptTempFile(prompt string) (string, func(), error) {
+ promptFile, err := os.CreateTemp("", "llm-prompt-*.md")
+ if err != nil {
+ return "", nil, fmt.Errorf("creating prompt file: %w", err)
+ }
+ tmpFile := promptFile.Name()
+ if _, err := promptFile.WriteString(prompt); err != nil {
+ _ = promptFile.Close()
+ _ = os.Remove(tmpFile)
+ return "", nil, fmt.Errorf("writing prompt: %w", err)
+ }
+ if err := promptFile.Close(); err != nil {
+ _ = os.Remove(tmpFile)
+ return "", nil, fmt.Errorf("closing prompt file: %w", err)
+ }
+
+ return tmpFile, func() { _ = os.Remove(tmpFile) }, nil
+}
diff --git a/internal/llm/llm_test.go b/internal/llm/llm_test.go
index 398971fe..c9e545f3 100644
--- a/internal/llm/llm_test.go
+++ b/internal/llm/llm_test.go
@@ -1,10 +1,15 @@
package llm
import (
+ "os"
"os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestAvailable(t *testing.T) {
@@ -24,3 +29,68 @@ func TestRunReturnsErrorWhenNoLLM(t *testing.T) {
assert.Error(t, err)
assert.Contains(t, err.Error(), "no LLM CLI found")
}
+
+func TestRunLongPromptUsesPrivateTempFile(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("Unix file modes are not portable to Windows")
+ }
+
+ binDir := t.TempDir()
+ claudePath := filepath.Join(binDir, "claude")
+ script := `#!/bin/sh
+prompt="$2"
+path=${prompt#Read the file at }
+path=${path% and follow the instructions inside it exactly.}
+stat -c %a "$path" 2>/dev/null || stat -f "%OLp" "$path"
+`
+ require.NoError(t, os.WriteFile(claudePath, []byte(script), 0700))
+ t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+ response, err := Run(strings.Repeat("x", 100001))
+ require.NoError(t, err)
+ assert.Equal(t, "600", response)
+}
+
+func TestRunShortPromptDoesNotCreateTempFile(t *testing.T) {
+ binDir := t.TempDir()
+ tmpDir := t.TempDir()
+ claudePath := filepath.Join(binDir, "claude")
+ script := `#!/bin/sh
+for f in "$TMPDIR"/llm-prompt-*.md; do
+ if [ -e "$f" ]; then
+ echo "unexpected temp file"
+ exit 1
+ fi
+done
+printf ok
+`
+ require.NoError(t, os.WriteFile(claudePath, []byte(script), 0700))
+ t.Setenv("PATH", binDir)
+ t.Setenv("TMPDIR", tmpDir)
+
+ response, err := Run("short prompt")
+ require.NoError(t, err)
+ assert.Equal(t, "ok", response)
+}
+
+func TestRunCodexFallbackDoesNotCreateTempFile(t *testing.T) {
+ binDir := t.TempDir()
+ tmpDir := t.TempDir()
+ codexPath := filepath.Join(binDir, "codex")
+ script := `#!/bin/sh
+for f in "$TMPDIR"/llm-prompt-*.md; do
+ if [ -e "$f" ]; then
+ echo "unexpected temp file"
+ exit 1
+ fi
+done
+printf ok
+`
+ require.NoError(t, os.WriteFile(codexPath, []byte(script), 0700))
+ t.Setenv("PATH", binDir)
+ t.Setenv("TMPDIR", tmpDir)
+
+ response, err := Run(strings.Repeat("x", 100001))
+ require.NoError(t, err)
+ assert.Equal(t, "ok", response)
+}
← 9a474335 fix(ci): trust queued Greptile gate
·
back to Cli Printing Press
·
fix(cli): verify-mode HTTP-verb short-circuit + N1 envelope 75e4d1b9 →