Skip to content

Commit 91dca54

Browse files
Merge pull request #11 from Sirz3chs/fix/hook-xargs-multiline-command
fix: replace xargs with head+sed in hook to handle multiline commands
2 parents 93ab331 + 14d7c15 commit 91dca54

File tree

2 files changed

+56
-1
lines changed

2 files changed

+56
-1
lines changed

internal/initcmd/init.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ if [ -z "$CMD" ]; then
3131
fi
3232
3333
# Extract the first command (before && or | or ;)
34-
FIRST_CMD=$(echo "$CMD" | sed 's/[;&|].*//' | xargs)
34+
# head -1 prevents xargs from seeing heredoc body lines with unmatched quotes
35+
FIRST_CMD=$(echo "$CMD" | head -1 | sed 's/[;&|].*//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
3536
3637
# Skip if already using snip
3738
case "$FIRST_CMD" in

internal/initcmd/init_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package initcmd
33
import (
44
"encoding/json"
55
"os"
6+
"os/exec"
67
"path/filepath"
8+
"strings"
79
"testing"
810
)
911

@@ -194,6 +196,58 @@ func TestUnpatchPreservesOtherHooks(t *testing.T) {
194196
}
195197
}
196198

199+
// TestHookScriptMultilineCommand verifies that the installed hook script handles
200+
// multiline commands (e.g. git commit with a heredoc) without error.
201+
// Previously, xargs was used to trim whitespace from FIRST_CMD and would fail
202+
// with exit 1 on unmatched quotes present in heredoc body lines.
203+
func TestHookScriptMultilineCommand(t *testing.T) {
204+
if _, err := exec.LookPath("bash"); err != nil {
205+
t.Skip("bash not available")
206+
}
207+
if _, err := exec.LookPath("jq"); err != nil {
208+
t.Skip("jq not available")
209+
}
210+
if _, err := exec.LookPath("snip"); err != nil {
211+
t.Skip("snip not available")
212+
}
213+
214+
// Write the hook to a temp file (simulates snip init).
215+
dir := t.TempDir()
216+
hookPath := filepath.Join(dir, "snip-rewrite.sh")
217+
if err := os.WriteFile(hookPath, []byte(hookScript), 0755); err != nil {
218+
t.Fatalf("write hook: %v", err)
219+
}
220+
221+
// Simulate the JSON Claude Code sends for a heredoc-style git commit.
222+
// The multiline command contains an unmatched `)"` on the last line,
223+
// which caused xargs to exit 1 (unmatched double quote).
224+
cmd := "git add file.go && git commit -m \"$(cat <<'EOF'\n fix: something\n\n Co-Authored-By: Bot <bot@example.com>\n EOF\n )\""
225+
payload, _ := json.Marshal(map[string]any{
226+
"tool_name": "Bash",
227+
"tool_input": map[string]any{"command": cmd},
228+
})
229+
230+
proc := exec.Command("bash", hookPath)
231+
proc.Stdin = strings.NewReader(string(payload))
232+
output, runErr := proc.Output()
233+
if runErr != nil {
234+
t.Fatalf("hook exited non-zero: %v", runErr)
235+
}
236+
237+
var result map[string]any
238+
if err := json.Unmarshal(output, &result); err != nil {
239+
t.Fatalf("hook output is not valid JSON: %v\noutput: %s", err, output)
240+
}
241+
242+
hookOut, _ := result["hookSpecificOutput"].(map[string]any)
243+
updated, _ := hookOut["updatedInput"].(map[string]any)
244+
rewritten, _ := updated["command"].(string)
245+
246+
if !strings.HasPrefix(rewritten, "snip git add ") {
247+
t.Errorf("expected rewritten command to start with 'snip git add', got: %s", rewritten)
248+
}
249+
}
250+
197251
func readSettings(t *testing.T, path string) map[string]any {
198252
t.Helper()
199253
data, err := os.ReadFile(path)

0 commit comments

Comments
 (0)