Type: Low-severity cleanup, batched so it can be one PR.
Validation gaps
- No client-side sandbox name ≤ 19 check despite the TODO at
internal/cli/sandbox.go:16 and the gateway's name exceeds maximum length (20 > 19) error being known (divergence log line 1274).
ParseLabels accepts empty keys while v1alpha1.Validate rejects them; sessionOpts.forward is unvalidated in Validate; deprecated flat fields + sessionOpts both set → new wins silently (warn); validate.go:85-91 workspace check is effectively dead.
ValidateCPU accepts Go-only forms (0x1p4, +2) the Rust CLI would reject.
ParseForwardSpec rejects [::1]:80-style IPv6 (bind becomes [::1] and is accepted verbatim).
namesFromManifest (internal/cli/sandbox_lifecycle.go:122-131) returns a slice but callers take names[0] (sandbox_cmd.go:68) — a multi-document manifest is silently truncated to the first document. Decision: error on more than one document for every command except create/delete (which may apply all), matching kubectl semantics; test both. (Policy multi-doc handling: item 25.)
Dead / contradictory code
notImplementedLeaf (internal/cli/sandbox.go:57) unused; the keep field (internal/cli/sandbox_create.go:224, populated at :68) is never read; SkipProviderMessage (pkg/sandbox/providers.go:146) never called though pkg/sandbox/create.go:224 says a message is printed; readAll/bytesReader (internal/cli/sandbox_create.go:26-27) are one-line wrappers over io.ReadAll/bytes.NewReader; pkg/policyyaml/serialize.go:197 reimplements strings.IndexByte. (bidiStream.cancel is owned by item 7; the stale create.go:16-19 comments by item 2.)
- Upload's
last_sandbox fallback (internal/cli/sandbox_transfer.go:34) is unreachable because parseUploadArgs errors on one arg despite RangeArgs(1,3) — either make one-arg upload work (NAME from last_sandbox) or tighten to RangeArgs(2,3).
Duplication hurting maintainability
withGatewayTarget / workspace() / resolveNameFromFileOrArgs / saveLastSandbox boilerplate copied across ten commands (a runWithSandbox helper — item 1 — fixes the -f ordering bug in one place).
- Two upload FS resolvers with different semantics:
runCreateUploads (internal/cli/sandbox_create.go) roots transfer.OSFS at /, the upload subcommand (resolveUploadFS, sandbox_transfer.go) at cwd, so findRepoRoot (pkg/transfer/gitignore.go:63) can find an enclosing .git in one path and not the other. Use one resolver.
envKeyRe duplicated in pkg/api/v1alpha1/validate.go:68 and pkg/sandbox/envlabel.go:10; WatchUntilReady (pkg/sandbox/watch.go:89) vs watchUntilPhase (pkg/sandbox/lifecycle.go:64) overlap; two raw-mode/SIGWINCH stacks (item 15).
gitignore / tar minor
pkg/transfer/gitignore.go:126-147: a nested non-anchored pattern with an inner slash (build/out in sub/.gitignore) is rewritten to sub/**/build/out, and go-gitignore prefixes ^(|.*/), so it matches at any depth (over-exclusion). No directory pruning, so ! can re-include under an excluded dir (git cannot). Expected semantics (git): a pattern containing a slash is anchored to the directory of the .gitignore that contains it (sub/build/out only), and once a directory is excluded nothing beneath it can be re-included. Implement those two rules; if the "gitignore-compatible, not git-index-identical" caveat (plan Decision 8) is kept, document these specific deviations in README.
- Upload tar headers carry local uid/gid/uname (
pkg/transfer/tar.go:45,61) — normalise to 0/root or the sandbox user.
clampU16 (pkg/policyyaml/serialize.go:206) silently clamps ports > 65535 instead of erroring.
Acceptance
Each fixed item has a unit test; dead symbols deleted (not commented out); golangci-lint unused passes.
Generated by Claude Code
Type: Low-severity cleanup, batched so it can be one PR.
Validation gaps
internal/cli/sandbox.go:16and the gateway'sname exceeds maximum length (20 > 19)error being known (divergence log line 1274).ParseLabelsaccepts empty keys whilev1alpha1.Validaterejects them;sessionOpts.forwardis unvalidated inValidate; deprecated flat fields +sessionOptsboth set → new wins silently (warn);validate.go:85-91workspace check is effectively dead.ValidateCPUaccepts Go-only forms (0x1p4,+2) the Rust CLI would reject.ParseForwardSpecrejects[::1]:80-style IPv6 (bind becomes[::1]and is accepted verbatim).namesFromManifest(internal/cli/sandbox_lifecycle.go:122-131) returns a slice but callers takenames[0](sandbox_cmd.go:68) — a multi-document manifest is silently truncated to the first document. Decision: error on more than one document for every command exceptcreate/delete(which may apply all), matchingkubectlsemantics; test both. (Policy multi-doc handling: item 25.)Dead / contradictory code
notImplementedLeaf(internal/cli/sandbox.go:57) unused; thekeepfield (internal/cli/sandbox_create.go:224, populated at:68) is never read;SkipProviderMessage(pkg/sandbox/providers.go:146) never called thoughpkg/sandbox/create.go:224says a message is printed;readAll/bytesReader(internal/cli/sandbox_create.go:26-27) are one-line wrappers overio.ReadAll/bytes.NewReader;pkg/policyyaml/serialize.go:197reimplementsstrings.IndexByte. (bidiStream.cancelis owned by item 7; the stalecreate.go:16-19comments by item 2.)last_sandboxfallback (internal/cli/sandbox_transfer.go:34) is unreachable becauseparseUploadArgserrors on one arg despiteRangeArgs(1,3)— either make one-arg upload work (NAME from last_sandbox) or tighten toRangeArgs(2,3).Duplication hurting maintainability
withGatewayTarget/workspace()/resolveNameFromFileOrArgs/saveLastSandboxboilerplate copied across ten commands (arunWithSandboxhelper — item 1 — fixes the-fordering bug in one place).runCreateUploads(internal/cli/sandbox_create.go) rootstransfer.OSFSat/, theuploadsubcommand (resolveUploadFS,sandbox_transfer.go) at cwd, sofindRepoRoot(pkg/transfer/gitignore.go:63) can find an enclosing.gitin one path and not the other. Use one resolver.envKeyReduplicated inpkg/api/v1alpha1/validate.go:68andpkg/sandbox/envlabel.go:10;WatchUntilReady(pkg/sandbox/watch.go:89) vswatchUntilPhase(pkg/sandbox/lifecycle.go:64) overlap; two raw-mode/SIGWINCH stacks (item 15).gitignore / tar minor
pkg/transfer/gitignore.go:126-147: a nested non-anchored pattern with an inner slash (build/outinsub/.gitignore) is rewritten tosub/**/build/out, and go-gitignore prefixes^(|.*/), so it matches at any depth (over-exclusion). No directory pruning, so!can re-include under an excluded dir (git cannot). Expected semantics (git): a pattern containing a slash is anchored to the directory of the.gitignorethat contains it (sub/build/outonly), and once a directory is excluded nothing beneath it can be re-included. Implement those two rules; if the "gitignore-compatible, not git-index-identical" caveat (plan Decision 8) is kept, document these specific deviations in README.pkg/transfer/tar.go:45,61) — normalise to 0/rootor the sandbox user.clampU16(pkg/policyyaml/serialize.go:206) silently clamps ports > 65535 instead of erroring.Acceptance
Each fixed item has a unit test; dead symbols deleted (not commented out);
golangci-lintunusedpasses.Generated by Claude Code