Skip to content

Commit 1e6b710

Browse files
fix: reject unproxyable commands like cd with clear error
cd must run in the parent shell to change directory. Running it through snip silently succeeded (exit 0) but had no effect, confusing AI agents into thinking the directory changed. Now snip exits with code 1 and a clear error message for commands that cannot be proxied (cd, source, .). Fixes #18
1 parent 2d3c761 commit 1e6b710

File tree

3 files changed

+52
-1
lines changed

3 files changed

+52
-1
lines changed

internal/cli/cli.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ func Run(args []string) int {
3838
command := remaining[0]
3939
cmdArgs := remaining[1:]
4040

41+
// Commands that cannot be proxied: they must run in the parent shell
42+
// to have any effect. Running them in a subprocess is a silent no-op.
43+
if unproxyableReason(command) != "" {
44+
fmt.Fprintf(os.Stderr, "snip: %s cannot be proxied (%s)\n", command, unproxyableReason(command))
45+
return 1
46+
}
47+
4148
// Built-in commands
4249
switch command {
4350
case "init":
@@ -171,6 +178,18 @@ Examples:
171178
fmt.Printf(usage, version)
172179
}
173180

181+
// unproxyableReason returns a human-readable reason if the command cannot be
182+
// proxied through an external process, or "" if it can.
183+
func unproxyableReason(command string) string {
184+
switch command {
185+
case "cd":
186+
return "it must run in the parent shell to change directory"
187+
case "source", ".":
188+
return "it must run in the parent shell to modify the environment"
189+
}
190+
return ""
191+
}
192+
174193
// Version returns the current version string.
175194
func Version() string {
176195
return version

internal/cli/cli_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package cli
2+
3+
import "testing"
4+
5+
func TestUnproxyableCommands(t *testing.T) {
6+
tests := []struct {
7+
command string
8+
want bool
9+
}{
10+
{"cd", true},
11+
{"source", true},
12+
{".", true},
13+
{"git", false},
14+
{"go", false},
15+
{"export", false},
16+
}
17+
18+
for _, tt := range tests {
19+
t.Run(tt.command, func(t *testing.T) {
20+
got := unproxyableReason(tt.command) != ""
21+
if got != tt.want {
22+
t.Errorf("unproxyableReason(%q) returned %q, wantBlocked=%v", tt.command, unproxyableReason(tt.command), tt.want)
23+
}
24+
})
25+
}
26+
}
27+
28+
func TestRunRejectsCd(t *testing.T) {
29+
code := Run([]string{"snip", "cd", "/tmp"})
30+
if code != 1 {
31+
t.Errorf("Run(cd) = %d, want 1", code)
32+
}
33+
}

internal/engine/executor.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ var shellBuiltins = map[string]bool{
2525
"source": true,
2626
"alias": true,
2727
"unalias": true,
28-
"cd": true,
2928
"eval": true,
3029
"set": true,
3130
"shopt": true,

0 commit comments

Comments
 (0)