Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ code-vm # interactive shell
| `code-vm secrets` | List secrets/vars the active profiles declare, mapped or not |
| `code-vm doctor` | Check host prerequisites |

Guest passthrough needs the `--`. Anything else is an unknown command,
refused on the host before the VM is touched: `code-vm claude -p ...` is a
typo, `code-vm -- claude -p ...` is the command you meant. That also means
invoking a subcommand your installed binary is too old to have fails with
`unknown command`, instead of being forwarded to the guest to fail there.

## Configuration

`~/.config/code-vm/config.yaml`:
Expand Down
23 changes: 22 additions & 1 deletion internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func NewRootCmd() *cobra.Command {
"directory: that directory becomes the working directory in the guest.",
SilenceUsage: true,
SilenceErrors: true,
Args: cobra.ArbitraryArgs,
Args: rejectUnknownCommand,
RunE: func(cmd *cobra.Command, args []string) error {
return runDefault(cmd.Context(), args)
},
Expand All @@ -43,6 +43,27 @@ func NewRootCmd() *cobra.Command {
return root
}

// rejectUnknownCommand accepts the two forms the root command really has — a
// bare invocation (interactive shell) and `code-vm -- <cmd> ...` (run <cmd>
// as the agent) — and refuses anything else on the host.
//
// Passthrough forces the root command to accept arbitrary args, which used to
// mean cobra handed any word it did not recognize as a subcommand straight to
// the guest. A typo, or a binary older than the subcommand being invoked, then
// failed as the guest shell's `exec: profile: not found` (exit 127) after the
// VM had already been booted and entered — an error naming neither the real
// problem nor the host. ArgsLenAtDash reports how many args preceded `--`
// (-1 when there is no `--`), so 0 is exactly the passthrough form.
func rejectUnknownCommand(cmd *cobra.Command, args []string) error {
if len(args) == 0 || cmd.ArgsLenAtDash() == 0 {
return nil
}
path := cmd.CommandPath()
return fmt.Errorf("unknown command %q for %q\n"+
"Run %q for the command list, or %q to run it in the sandbox",
args[0], path, path+" --help", path+" -- "+args[0]+" ...")
}

// Execute runs the CLI.
func Execute() error {
return NewRootCmd().Execute()
Expand Down
43 changes: 43 additions & 0 deletions internal/cli/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,46 @@ func TestLoadConfigCanonicalizesSymlinkedConfigDir(t *testing.T) {
t.Errorf("loadConfig error = %v, want a MountsExclude refusal", err)
}
}

// The root command takes arbitrary args because a bare `code-vm -- <cmd>`
// forwards them to the guest. That must not extend to args with no `--` in
// front of them: cobra would hand any unrecognized word to the guest, so a
// typo — or a stale binary that predates a subcommand the caller expects —
// surfaced as the guest shell's `exec: profile: not found` and exit 127
// instead of a host-side "unknown command". Nothing may reach the VM.
func TestRootRejectsUnknownSubcommandBeforeTouchingTheVM(t *testing.T) {
root := NewRootCmd()
setupShellFixture(t)

r := installFakeClient(t, "")
root.SetArgs([]string{"profile-typo", "add", "git@example.com:x/y.git"})
err := root.Execute()
if err == nil {
t.Fatal("unknown subcommand = nil error, want a host-side refusal")
}
if !strings.Contains(err.Error(), `unknown command "profile-typo"`) {
t.Errorf("error = %v, want it to name the unknown command", err)
}
if !strings.Contains(err.Error(), "--") {
t.Errorf("error = %v, want it to point at `--` for sandbox passthrough", err)
}
if len(r.calls) != 0 {
t.Errorf("nothing may reach the VM for an unknown command, calls=%v", r.calls)
}
}

// The counterpart: an explicit `--` still forwards everything after it to the
// guest verbatim, including words that collide with host subcommand names.
func TestRootPassesArgsAfterDoubleDashToTheGuest(t *testing.T) {
root := NewRootCmd()
setupShellFixture(t)

r := installFakeClient(t, "Running")
root.SetArgs([]string{"--", "claude", "login"})
if err := root.Execute(); err != nil {
t.Fatalf("`-- claude login` = %v, want passthrough", err)
}
if !ranAny(r.calls, "claude login") {
t.Errorf("guest command not forwarded, calls=%v", r.calls)
}
}
9 changes: 9 additions & 0 deletions test-vm-sandbox.sh
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,15 @@ else
fail "allow rejects malformed input (got: $MALFORMED_OUT)"
fi

# Host-side, no VM needed: an unrecognized word must not be forwarded into the
# guest. Captured, not piped: code-vm exits non-zero here by design.
UNKNOWN_OUT=$("${CODE_VM_ARGS[@]}" profile-typo add x 2>&1)
if echo "$UNKNOWN_OUT" | grep -q 'unknown command "profile-typo"'; then
pass "an unknown subcommand is refused on the host"
else
fail "an unknown subcommand is refused on the host (got: $UNKNOWN_OUT)"
fi

# Removing the domain from the config must take effect, or a revoked domain
# would stay allowed for the VM's lifetime.
cp "$CONFIG_FILE.suite-backup" "$CONFIG_FILE"
Expand Down
Loading