You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
internal/deps/scripts/moat-init.sh has grown to ~440 lines of POSIX shell handling host entries, SSH agent bridging, agent config setup (Claude/Codex/Gemini), Docker-in-Docker, clipboard bridging, git config, pre-run hooks, and privilege dropping. Each new feature adds conditional blocks with retry loops and error handling that's hard to test and brittle across distros.
Replace it with a small, statically-linked Go binary that gets bind-mounted into the container at runtime.
Motivation
Testability: Shell logic is tested only via E2E. A Go binary can be unit-tested for each init phase (host entry writing, SSH bridging, file staging, privilege dropping).
Portability: Shell assumptions (e.g., getent ahostsv4 availability, GNU stat -c format, socat presence) vary across base images. Go's stdlib handles DNS, file ops, and user lookup natively.
Error handling: set -e provides coarse-grained error handling. Go gives structured errors per phase with clear reporting.
Complexity ceiling: The script is manageable today but each new feature (most recently DNS-resolved host entries in fix(proxy): reach host from custom networks on Docker Desktop #337) adds conditional paths that compound. A compiled binary provides a better foundation for future growth.
Approach
Build cmd/moat-init/main.go — reads the same MOAT_* env vars, performs the same setup phases in order.
Cross-compile as a static binary (CGO_ENABLED=0) during moat run image build or cache it.
Bind-mount the binary into the container (e.g., /usr/local/bin/moat-init) and use it as the entrypoint wrapper.
Each init phase becomes a function with its own tests — host entries, SSH bridge, agent config copy, DinD, git config, pre-run hook, privilege drop.
Remove the shell script once the binary is stable.
Considerations
Binary size: static Go binary will be ~5-10MB. Acceptable for a bind mount.
Architecture: need to match the container's target arch (typically linux/amd64 or linux/arm64). May need to cross-compile or detect at runtime.
gosu replacement: Go can drop privileges natively via syscall.Setuid/syscall.Setgid, eliminating the gosu dependency.
Backward compat: existing moat-init.sh keeps working until the binary is validated across all runtimes.
Signal to start
This becomes urgent when the next feature needs conditional logic in moat-init.sh. Until then, the shell script is functional.
Summary
internal/deps/scripts/moat-init.shhas grown to ~440 lines of POSIX shell handling host entries, SSH agent bridging, agent config setup (Claude/Codex/Gemini), Docker-in-Docker, clipboard bridging, git config, pre-run hooks, and privilege dropping. Each new feature adds conditional blocks with retry loops and error handling that's hard to test and brittle across distros.Replace it with a small, statically-linked Go binary that gets bind-mounted into the container at runtime.
Motivation
getent ahostsv4availability, GNUstat -cformat,socatpresence) vary across base images. Go's stdlib handles DNS, file ops, and user lookup natively.set -eprovides coarse-grained error handling. Go gives structured errors per phase with clear reporting.Approach
cmd/moat-init/main.go— reads the sameMOAT_*env vars, performs the same setup phases in order.CGO_ENABLED=0) duringmoat runimage build or cache it./usr/local/bin/moat-init) and use it as the entrypoint wrapper.Considerations
gosureplacement: Go can drop privileges natively viasyscall.Setuid/syscall.Setgid, eliminating thegosudependency.moat-init.shkeeps working until the binary is validated across all runtimes.Signal to start
This becomes urgent when the next feature needs conditional logic in moat-init.sh. Until then, the shell script is functional.