Repository: coder/envbuilder
Pull request #278 (2024-07-23 09:14:51)

chore: refactor options to separate package
Part of https://github.com/coder/team-coconut/issues/11
Extracts Options to its own package to support easier importing from separate repositories.

johnstcn: > I would suggest skipping the `pkg` prefix, otherwise LGTM. 👍🏻 We already have some packages, like `devcontainer` that would make sense to have on the same level IMO.

Fair enough. I only chose `/pkg/` due to it being a commonly-used prefix, but we can strip it.
mafredri: > Fair enough. I only chose `/pkg/` due to it being a commonly-used prefix, but we can strip it.

True, it's not totally uncommon, but it's a bit controversial. Also considered "legacy" https://x.com/bradfitz/status/1039512487538970624 by some 😄

Patch:
diff --git a/Makefile b/Makefile
index 28827ef..8bd3f6b 100644
--- a/Makefile
+++ b/Makefile
@@ -33,10 +33,10 @@ build: scripts/envbuilder-$(GOARCH)
 update-golden-files: .gen-golden
 
 .gen-golden: $(GOLDEN_FILES) $(GO_SRC_FILES) $(GO_TEST_FILES)
-	go test . -update
+	go test ./options -update
 	@touch "$@"
 
-docs: options.go
+docs: options/options.go options/options_test.go
 	go run ./scripts/docsgen/main.go
 
 .PHONY: test
diff --git a/cmd/envbuilder/main.go b/cmd/envbuilder/main.go
index 7740553..24d63fd 100644
--- a/cmd/envbuilder/main.go
+++ b/cmd/envbuilder/main.go
@@ -8,6 +8,8 @@ import (
 	"slices"
 	"strings"
 
+	"github.com/coder/envbuilder/options"
+
 	"github.com/coder/coder/v2/codersdk"
 	"github.com/coder/envbuilder"
 	"github.com/coder/envbuilder/internal/log"
@@ -23,47 +25,47 @@ func main() {
 	cmd := envbuilderCmd()
 	err := cmd.Invoke().WithOS().Run()
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "error: %v", err)
+		_, _ = fmt.Fprintf(os.Stderr, "error: %v", err)
 		os.Exit(1)
 	}
 }
 
 func envbuilderCmd() serpent.Command {
-	var options envbuilder.Options
+	var o options.Options
 	cmd := serpent.Command{
 		Use:     "envbuilder",
-		Options: options.CLI(),
+		Options: o.CLI(),
 		Handler: func(inv *serpent.Invocation) error {
-			options.Logger = log.New(os.Stderr, options.Verbose)
-			if options.CoderAgentURL != "" {
-				if options.CoderAgentToken == "" {
+			o.Logger = log.New(os.Stderr, o.Verbose)
+			if o.CoderAgentURL != "" {
+				if o.CoderAgentToken == "" {
 					return errors.New("CODER_AGENT_URL must be set if CODER_AGENT_TOKEN is set")
 				}
-				u, err := url.Parse(options.CoderAgentURL)
+				u, err := url.Parse(o.CoderAgentURL)
 				if err != nil {
 					return fmt.Errorf("unable to parse CODER_AGENT_URL as URL: %w", err)
 				}
-				coderLog, closeLogs, err := log.Coder(inv.Context(), u, options.CoderAgentToken)
+				coderLog, closeLogs, err := log.Coder(inv.Context(), u, o.CoderAgentToken)
 				if err == nil {
-					options.Logger = log.Wrap(options.Logger, coderLog)
+					o.Logger = log.Wrap(o.Logger, coderLog)
 					defer closeLogs()
 					// This adds the envbuilder subsystem.
 					// If telemetry is enabled in a Coder deployment,
 					// this will be reported and help us understand
 					// envbuilder usage.
-					if !slices.Contains(options.CoderAgentSubsystem, string(codersdk.AgentSubsystemEnvbuilder)) {
-						options.CoderAgentSubsystem = append(options.CoderAgentSubsystem, string(codersdk.AgentSubsystemEnvbuilder))
-						_ = os.Setenv("CODER_AGENT_SUBSYSTEM", strings.Join(options.CoderAgentSubsystem, ","))
+					if !slices.Contains(o.CoderAgentSubsystem, string(codersdk.AgentSubsystemEnvbuilder)) {
+						o.CoderAgentSubsystem = append(o.CoderAgentSubsystem, string(codersdk.AgentSubsystemEnvbuilder))
+						_ = os.Setenv("CODER_AGENT_SUBSYSTEM", strings.Join(o.CoderAgentSubsystem, ","))
 					}
 				} else {
 					// Failure to log to Coder should cause a fatal error.
-					options.Logger(log.LevelError, "unable to send logs to Coder: %s", err.Error())
+					o.Logger(log.LevelError, "unable to send logs to Coder: %s", err.Error())
 				}
 			}
 
-			err := envbuilder.Run(inv.Context(), options)
+			err := envbuilder.Run(inv.Context(), o)
 			if err != nil {
-				options.Logger(log.LevelError, "error: %s", err)
+				o.Logger(log.LevelError, "error: %s", err)
 			}
 			return err
 		},
diff --git a/envbuilder.go b/envbuilder.go
index 62c4279..3cbdab0 100644
--- a/envbuilder.go
+++ b/envbuilder.go
@@ -24,8 +24,7 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/kballard/go-shellquote"
-	"github.com/mattn/go-isatty"
+	"github.com/coder/envbuilder/options"
 
 	"github.com/GoogleContainerTools/kaniko/pkg/config"
 	"github.com/GoogleContainerTools/kaniko/pkg/creds"
@@ -46,6 +45,8 @@ import (
 	"github.com/go-git/go-git/v5/plumbing/transport"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
 	"github.com/google/go-containerregistry/pkg/v1/remote"
+	"github.com/kballard/go-shellquote"
+	"github.com/mattn/go-isatty"
 	"github.com/sirupsen/logrus"
 	"github.com/tailscale/hujson"
 	"golang.org/x/xerrors"
@@ -83,45 +84,45 @@ type DockerConfig configfile.ConfigFile
 // Logger is the logf to use for all operations.
 // Filesystem is the filesystem to use for all operations.
 // Defaults to the host filesystem.
-func Run(ctx context.Context, options Options) error {
+func Run(ctx context.Context, opts options.Options) error {
 	// Temporarily removed these from the default settings to prevent conflicts
 	// between current and legacy environment variables that add default values.
 	// Once the legacy environment variables are phased out, this can be
 	// reinstated to the previous default values.
-	if len(options.IgnorePaths) == 0 {
-		options.IgnorePaths = []string{
+	if len(opts.IgnorePaths) == 0 {
+		opts.IgnorePaths = []string{
 			"/var/run",
 			// KinD adds these paths to pods, so ignore them by default.
 			"/product_uuid", "/product_name",
 		}
 	}
-	if options.InitScript == "" {
-		options.InitScript = "sleep infinity"
+	if opts.InitScript == "" {
+		opts.InitScript = "sleep infinity"
 	}
-	if options.InitCommand == "" {
-		options.InitCommand = "/bin/sh"
+	if opts.InitCommand == "" {
+		opts.InitCommand = "/bin/sh"
 	}
-	if options.CacheRepo == "" && options.PushImage {
+	if opts.CacheRepo == "" && opts.PushImage {
 		return fmt.Errorf("--cache-repo must be set when using --push-image")
 	}
 	// Default to the shell!
-	initArgs := []string{"-c", options.InitScript}
-	if options.InitArgs != "" {
+	initArgs := []string{"-c", opts.InitScript}
+	if opts.InitArgs != "" {
 		var err error
-		initArgs, err = shellquote.Split(options.InitArgs)
+		initArgs, err = shellquote.Split(opts.InitArgs)
 		if err != nil {
 			return fmt.Errorf("parse init args: %w", err)
 		}
 	}
-	if options.Filesystem == nil {
-		options.Filesystem = &osfsWithChmod{osfs.New("/")}
+	if opts.Filesystem == nil {
+		opts.Filesystem = &osfsWithChmod{osfs.New("/")}
 	}
-	if options.WorkspaceFolder == "" {
-		f, err := DefaultWorkspaceFolder(options.GitURL)
+	if opts.WorkspaceFolder == "" {
+		f, err := DefaultWorkspaceFolder(opts.GitURL)
 		if err != nil {
 			return err
 		}
-		options.WorkspaceFolder = f
+		opts.WorkspaceFolder = f
 	}
 
 	stageNumber := 0
@@ -129,22 +130,22 @@ func Run(ctx context.Context, options Options) error {
 		now := time.Now()
 		stageNumber++
 		stageNum := stageNumber
-		options.Logger(log.LevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...))
+		opts.Logger(log.LevelInfo, "#%d: %s", stageNum, fmt.Sprintf(format, args...))
 
 		return func(format string, args ...any) {
-			options.Logger(log.LevelInfo, "#%d: %s [%s]", stageNum, fmt.Sprintf(format, args...), time.Since(now))
+			opts.Logger(log.LevelInfo, "#%d: %s [%s]", stageNum, fmt.Sprintf(format, args...), time.Since(now))
 		}
 	}
 
-	options.Logger(log.LevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"))
+	opts.Logger(log.LevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"))
 
 	var caBundle []byte
-	if options.SSLCertBase64 != "" {
+	if opts.SSLCertBase64 != "" {
 		certPool, err := x509.SystemCertPool()
 		if err != nil {
 			return xerrors.Errorf("get global system cert pool: %w", err)
 		}
-		data, err := base64.StdEncoding.DecodeString(options.SSLCertBase64)
+		data, err := base64.StdEncoding.DecodeString(opts.SSLCertBase64)
 		if err != nil {
 			return xerrors.Errorf("base64 decode ssl cert: %w", err)
 		}
@@ -155,8 +156,8 @@ func Run(ctx context.Context, options Options) error {
 		caBundle = data
 	}
 
-	if options.DockerConfigBase64 != "" {
-		decoded, err := base64.StdEncoding.DecodeString(options.DockerConfigBase64)
+	if opts.DockerConfigBase64 != "" {
+		decoded, err := base64.StdEncoding.DecodeString(opts.DockerConfigBase64)
 		if err != nil {
 			return fmt.Errorf("decode docker config: %w", err)
 		}
@@ -177,10 +178,10 @@ func Run(ctx context.Context, options Options) error {
 
 	var fallbackErr error
 	var cloned bool
-	if options.GitURL != "" {
+	if opts.GitURL != "" {
 		endStage := startStage("📦 Cloning %s to %s...",
-			newColor(color.FgCyan).Sprintf(options.GitURL),
-			newColor(color.FgCyan).Sprintf(options.WorkspaceFolder),
+			newColor(color.FgCyan).Sprintf(opts.GitURL),
+			newColor(color.FgCyan).Sprintf(opts.WorkspaceFolder),
 		)
 
 		reader, writer := io.Pipe()
@@ -198,28 +199,28 @@ func Run(ctx context.Context, options Options) error {
 					if line == "" {
 						continue
 					}
-					options.Logger(log.LevelInfo, "#1: %s", strings.TrimSpace(line))
+					opts.Logger(log.LevelInfo, "#1: %s", strings.TrimSpace(line))
 				}
 			}
 		}()
 
 		cloneOpts := CloneRepoOptions{
-			Path:         options.WorkspaceFolder,
-			Storage:      options.Filesystem,
-			Insecure:     options.Insecure,
+			Path:         opts.WorkspaceFolder,
+			Storage:      opts.Filesystem,
+			Insecure:     opts.Insecure,
 			Progress:     writer,
-			SingleBranch: options.GitCloneSingleBranch,
-			Depth:        int(options.GitCloneDepth),
+			SingleBranch: opts.GitCloneSingleBranch,
+			Depth:        int(opts.GitCloneDepth),
 			CABundle:     caBundle,
 		}
 
-		cloneOpts.RepoAuth = SetupRepoAuth(&options)
-		if options.GitHTTPProxyURL != "" {
+		cloneOpts.RepoAuth = SetupRepoAuth(&opts)
+		if opts.GitHTTPProxyURL != "" {
 			cloneOpts.ProxyOptions = transport.ProxyOptions{
-				URL: options.GitHTTPProxyURL,
+				URL: opts.GitHTTPProxyURL,
 			}
 		}
-		cloneOpts.RepoURL = options.GitURL
+		cloneOpts.RepoURL = opts.GitURL
 
 		cloned, fallbackErr = CloneRepo(ctx, cloneOpts)
 		if fallbackErr == nil {
@@ -229,19 +230,19 @@ func Run(ctx context.Context, options Options) error {
 				endStage("📦 The repository already exists!")
 			}
 		} else {
-			options.Logger(log.LevelError, "Failed to clone repository: %s", fallbackErr.Error())
-			options.Logger(log.LevelError, "Falling back to the default image...")
+			opts.Logger(log.LevelError, "Failed to clone repository: %s", fallbackErr.Error())
+			opts.Logger(log.LevelError, "Falling back to the default image...")
 		}
 	}
 
 	defaultBuildParams := func() (*devcontainer.Compiled, error) {
 		dockerfile := filepath.Join(MagicDir, "Dockerfile")
-		file, err := options.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0o644)
+		file, err := opts.Filesystem.OpenFile(dockerfile, os.O_CREATE|os.O_WRONLY, 0o644)
 		if err != nil {
 			return nil, err
 		}
 		defer file.Close()
-		if options.FallbackImage == "" {
+		if opts.FallbackImage == "" {
 			if fallbackErr != nil {
 				return nil, xerrors.Errorf("%s: %w", fallbackErr.Error(), ErrNoFallbackImage)
 			}
@@ -249,7 +250,7 @@ func Run(ctx context.Context, options Options) error {
 			// don't support parsing a multiline error.
 			return nil, ErrNoFallbackImage
 		}
-		content := "FROM " + options.FallbackImage
+		content := "FROM " + opts.FallbackImage
 		_, err = file.Write([]byte(content))
 		if err != nil {
 			return nil, err
@@ -267,19 +268,19 @@ func Run(ctx context.Context, options Options) error {
 
 		devcontainerPath string
 	)
-	if options.DockerfilePath == "" {
+	if opts.DockerfilePath == "" {
 		// Only look for a devcontainer if a Dockerfile wasn't specified.
 		// devcontainer is a standard, so it's reasonable to be the default.
 		var devcontainerDir string
 		var err error
-		devcontainerPath, devcontainerDir, err = findDevcontainerJSON(options)
+		devcontainerPath, devcontainerDir, err = findDevcontainerJSON(opts)
 		if err != nil {
-			options.Logger(log.LevelError, "Failed to locate devcontainer.json: %s", err.Error())
-			options.Logger(log.LevelError, "Falling back to the default image...")
+			opts.Logger(log.LevelError, "Failed to locate devcontainer.json: %s", err.Error())
+			opts.Logger(log.LevelError, "Falling back to the default image...")
 		} else {
 			// We know a devcontainer exists.
 			// Let's parse it and use it!
-			file, err := options.Filesystem.Open(devcontainerPath)
+			file, err := opts.Filesystem.Open(devcontainerPath)
 			if err != nil {
 				return fmt.Errorf("open devcontainer.json: %w", err)
 			}
@@ -296,32 +297,32 @@ func Run(ctx context.Context, options Options) error {
 					if err != nil {
 						return fmt.Errorf("no Dockerfile or image found: %w", err)
 					}
-					options.Logger(log.LevelInfo, "No Dockerfile or image specified; falling back to the default image...")
+					opts.Logger(log.LevelInfo, "No Dockerfile or image specified; falling back to the default image...")
 					fallbackDockerfile = defaultParams.DockerfilePath
 				}
-				buildParams, err = devContainer.Compile(options.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, options.WorkspaceFolder, false, os.LookupEnv)
+				buildParams, err = devContainer.Compile(opts.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, opts.WorkspaceFolder, false, os.LookupEnv)
 				if err != nil {
 					return fmt.Errorf("compile devcontainer.json: %w", err)
 				}
 				scripts = devContainer.LifecycleScripts
 			} else {
-				options.Logger(log.LevelError, "Failed to parse devcontainer.json: %s", err.Error())
-				options.Logger(log.LevelError, "Falling back to the default image...")
+				opts.Logger(log.LevelError, "Failed to parse devcontainer.json: %s", err.Error())
+				opts.Logger(log.LevelError, "Falling back to the default image...")
 			}
 		}
 	} else {
 		// If a Dockerfile was specified, we use that.
-		dockerfilePath := filepath.Join(options.WorkspaceFolder, options.DockerfilePath)
+		dockerfilePath := filepath.Join(opts.WorkspaceFolder, opts.DockerfilePath)
 
 		// If the dockerfilePath is specified and deeper than the base of WorkspaceFolder AND the BuildContextPath is
 		// not defined, show a warning
 		dockerfileDir := filepath.Dir(dockerfilePath)
-		if dockerfileDir != filepath.Clean(options.WorkspaceFolder) && options.BuildContextPath == "" {
-			options.Logger(log.LevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, options.WorkspaceFolder)
-			options.Logger(log.LevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir)
+		if dockerfileDir != filepath.Clean(opts.WorkspaceFolder) && opts.BuildContextPath == "" {
+			opts.Logger(log.LevelWarn, "given dockerfile %q is below %q and no custom build context has been defined", dockerfilePath, opts.WorkspaceFolder)
+			opts.Logger(log.LevelWarn, "\t-> set BUILD_CONTEXT_PATH to %q to fix", dockerfileDir)
 		}
 
-		dockerfile, err := options.Filesystem.Open(dockerfilePath)
+		dockerfile, err := opts.Filesystem.Open(dockerfilePath)
 		if err == nil {
 			content, err := io.ReadAll(dockerfile)
 			if err != nil {
@@ -330,7 +331,7 @@ func Run(ctx context.Context, options Options) error {
 			buildParams = &devcontainer.Compiled{
 				DockerfilePath:    dockerfilePath,
 				DockerfileContent: string(content),
-				BuildContext:      filepath.Join(options.WorkspaceFolder, options.BuildContextPath),
+				BuildContext:      filepath.Join(opts.WorkspaceFolder, opts.BuildContextPath),
 			}
 		}
 	}
@@ -347,17 +348,17 @@ func Run(ctx context.Context, options Options) error {
 
 	HijackLogrus(func(entry *logrus.Entry) {
 		for _, line := range strings.Split(entry.Message, "\r") {
-			options.Logger(log.LevelInfo, "#%d: %s", stageNumber, color.HiBlackString(line))
+			opts.Logger(log.LevelInfo, "#%d: %s", stageNumber, color.HiBlackString(line))
 		}
 	})
 
 	var closeAfterBuild func()
 	// Allows quick testing of layer caching using a local directory!
-	if options.LayerCacheDir != "" {
+	if opts.LayerCacheDir != "" {
 		cfg := &configuration.Configuration{
 			Storage: configuration.Storage{
 				"filesystem": configuration.Parameters{
-					"rootdirectory": options.LayerCacheDir,
+					"rootdirectory": opts.LayerCacheDir,
 				},
 			},
 		}
@@ -380,31 +381,31 @@ func Run(ctx context.Context, options Options) error {
 		go func() {
 			err := srv.Serve(listener)
 			if err != nil && !errors.Is(err, http.ErrServerClosed) {
-				options.Logger(log.LevelError, "Failed to serve registry: %s", err.Error())
+				opts.Logger(log.LevelError, "Failed to serve registry: %s", err.Error())
 			}
 		}()
 		closeAfterBuild = func() {
 			_ = srv.Close()
 			_ = listener.Close()
 		}
-		if options.CacheRepo != "" {
-			options.Logger(log.LevelWarn, "Overriding cache repo with local registry...")
+		if opts.CacheRepo != "" {
+			opts.Logger(log.LevelWarn, "Overriding cache repo with local registry...")
 		}
-		options.CacheRepo = fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port)
+		opts.CacheRepo = fmt.Sprintf("localhost:%d/local/cache", tcpAddr.Port)
 	}
 
-	// IgnorePaths in the Kaniko options doesn't properly ignore paths.
+	// IgnorePaths in the Kaniko opts doesn't properly ignore paths.
 	// So we add them to the default ignore list. See:
 	// https://github.com/GoogleContainerTools/kaniko/blob/63be4990ca5a60bdf06ddc4d10aa4eca0c0bc714/cmd/executor/cmd/root.go#L136
 	ignorePaths := append([]string{
 		MagicDir,
-		options.WorkspaceFolder,
+		opts.WorkspaceFolder,
 		// See: https://github.com/coder/envbuilder/issues/37
 		"/etc/resolv.conf",
-	}, options.IgnorePaths...)
+	}, opts.IgnorePaths...)
 
-	if options.LayerCacheDir != "" {
-		ignorePaths = append(ignorePaths, options.LayerCacheDir)
+	if opts.LayerCacheDir != "" {
+		ignorePaths = append(ignorePaths, opts.LayerCacheDir)
 	}
 
 	for _, ignorePath := range ignorePaths {
@@ -417,7 +418,7 @@ func Run(ctx context.Context, options Options) error {
 
 	// In order to allow 'resuming' envbuilder, embed the binary into the image
 	// if it is being pushed
-	if options.PushImage {
+	if opts.PushImage {
 		exePath, err := os.Executable()
 		if err != nil {
 			return xerrors.Errorf("get exe path: %w", err)
@@ -443,10 +444,10 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 	// ignorePrefixes is a superset of ignorePaths that we pass to kaniko's
 	// IgnoreList.
 	ignorePrefixes := append([]string{"/dev", "/proc", "/sys"}, ignorePaths...)
-	restoreMounts, err := ebutil.TempRemount(options.Logger, tempRemountDest, ignorePrefixes...)
+	restoreMounts, err := ebutil.TempRemount(opts.Logger, tempRemountDest, ignorePrefixes...)
 	defer func() { // restoreMounts should never be nil
 		if err := restoreMounts(); err != nil {
-			options.Logger(log.LevelError, "restore mounts: %s", err.Error())
+			opts.Logger(log.LevelError, "restore mounts: %s", err.Error())
 		}
 	}()
 	if err != nil {
@@ -455,8 +456,8 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 
 	skippedRebuild := false
 	build := func() (v1.Image, error) {
-		_, err := options.Filesystem.Stat(MagicFile)
-		if err == nil && options.SkipRebuild {
+		_, err := opts.Filesystem.Stat(MagicFile)
+		if err == nil && opts.SkipRebuild {
 			endStage := startStage("🏗️ Skipping build because of cache...")
 			imageRef, err := devcontainer.ImageFromDockerfile(buildParams.DockerfileContent)
 			if err != nil {
@@ -479,7 +480,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 
 		// It's possible that the container will already have files in it, and
 		// we don't want to merge a new container with the old one.
-		if err := maybeDeleteFilesystem(options.Logger, options.ForceSafe); err != nil {
+		if err := maybeDeleteFilesystem(opts.Logger, opts.ForceSafe); err != nil {
 			return nil, fmt.Errorf("delete filesystem: %w", err)
 		}
 
@@ -492,18 +493,18 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 		go func() {
 			scanner := bufio.NewScanner(stdoutReader)
 			for scanner.Scan() {
-				options.Logger(log.LevelInfo, "%s", scanner.Text())
+				opts.Logger(log.LevelInfo, "%s", scanner.Text())
 			}
 		}()
 		go func() {
 			scanner := bufio.NewScanner(stderrReader)
 			for scanner.Scan() {
-				options.Logger(log.LevelInfo, "%s", scanner.Text())
+				opts.Logger(log.LevelInfo, "%s", scanner.Text())
 			}
 		}()
 		cacheTTL := time.Hour * 24 * 7
-		if options.CacheTTLDays != 0 {
-			cacheTTL = time.Hour * 24 * time.Duration(options.CacheTTLDays)
+		if opts.CacheTTLDays != 0 {
+			cacheTTL = time.Hour * 24 * time.Duration(opts.CacheTTLDays)
 		}
 
 		// At this point we have all the context, we can now build!
@@ -512,10 +513,10 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 			registryMirror = strings.Split(val, ";")
 		}
 		var destinations []string
-		if options.CacheRepo != "" {
-			destinations = append(destinations, options.CacheRepo)
+		if opts.CacheRepo != "" {
+			destinations = append(destinations, opts.CacheRepo)
 		}
-		opts := &config.KanikoOptions{
+		kOpts := &config.KanikoOptions{
 			// Boilerplate!
 			CustomPlatform:    platforms.Format(platforms.Normalize(platforms.DefaultSpec())),
 			SnapshotMode:      "redo",
@@ -523,7 +524,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 			RunStdout:         stdoutWriter,
 			RunStderr:         stderrWriter,
 			Destinations:      destinations,
-			NoPush:            !options.PushImage || len(destinations) == 0,
+			NoPush:            !opts.PushImage || len(destinations) == 0,
 			CacheRunLayers:    true,
 			CacheCopyLayers:   true,
 			CompressedCaching: true,
@@ -535,18 +536,18 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 			CacheOptions: config.CacheOptions{
 				// Cache for a week by default!
 				CacheTTL: cacheTTL,
-				CacheDir: options.BaseImageCacheDir,
+				CacheDir: opts.BaseImageCacheDir,
 			},
 			ForceUnpack:       true,
 			BuildArgs:         buildParams.BuildArgs,
-			CacheRepo:         options.CacheRepo,
-			Cache:             options.CacheRepo != "" || options.BaseImageCacheDir != "",
+			CacheRepo:         opts.CacheRepo,
+			Cache:             opts.CacheRepo != "" || opts.BaseImageCacheDir != "",
 			DockerfilePath:    buildParams.DockerfilePath,
 			DockerfileContent: buildParams.DockerfileContent,
 			RegistryOptions: config.RegistryOptions{
-				Insecure:      options.Insecure,
-				InsecurePull:  options.Insecure,
-				SkipTLSVerify: options.Insecure,
+				Insecure:      opts.Insecure,
+				InsecurePull:  opts.Insecure,
+				SkipTLSVerify: opts.Insecure,
 				// Enables registry mirror features in Kaniko, see more in link below
 				// https://github.com/GoogleContainerTools/kaniko?tab=readme-ov-file#flag---registry-mirror
 				// Related to PR #114
@@ -556,12 +557,12 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 			SrcContext: buildParams.BuildContext,
 
 			// For cached image utilization, produce reproducible builds.
-			Reproducible: options.PushImage,
+			Reproducible: opts.PushImage,
 		}
 
-		if options.GetCachedImage {
+		if opts.GetCachedImage {
 			endStage := startStage("🏗️ Checking for cached image...")
-			image, err := executor.DoCacheProbe(opts)
+			image, err := executor.DoCacheProbe(kOpts)
 			if err != nil {
 				return nil, xerrors.Errorf("get cached image: %w", err)
 			}
@@ -570,19 +571,19 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 				return nil, xerrors.Errorf("get cached image digest: %w", err)
 			}
 			endStage("🏗️ Found cached image!")
-			_, _ = fmt.Fprintf(os.Stdout, "ENVBUILDER_CACHED_IMAGE=%s@%s\n", options.CacheRepo, digest.String())
+			_, _ = fmt.Fprintf(os.Stdout, "ENVBUILDER_CACHED_IMAGE=%s@%s\n", kOpts.CacheRepo, digest.String())
 			os.Exit(0)
 		}
 
 		endStage := startStage("🏗️ Building image...")
-		image, err := executor.DoBuild(opts)
+		image, err := executor.DoBuild(kOpts)
 		if err != nil {
 			return nil, xerrors.Errorf("do build: %w", err)
 		}
 		endStage("🏗️ Built image!")
-		if options.PushImage {
+		if opts.PushImage {
 			endStage = startStage("🏗️ Pushing image...")
-			if err := executor.DoPush(image, opts); err != nil {
+			if err := executor.DoPush(image, kOpts); err != nil {
 				return nil, xerrors.Errorf("do push: %w", err)
 			}
 			endStage("🏗️ Pushed image!")
@@ -611,13 +612,13 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 			fallback = true
 			fallbackErr = err
 		case strings.Contains(err.Error(), "unexpected status code 401 Unauthorized"):
-			options.Logger(log.LevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!")
+			opts.Logger(log.LevelError, "Unable to pull the provided image. Ensure your registry credentials are correct!")
 		}
-		if !fallback || options.ExitOnBuildFailure {
+		if !fallback || opts.ExitOnBuildFailure {
 			return err
 		}
-		options.Logger(log.LevelError, "Failed to build: %s", err)
-		options.Logger(log.LevelError, "Falling back to the default image...")
+		opts.Logger(log.LevelError, "Failed to build: %s", err)
+		opts.Logger(log.LevelError, "Falling back to the default image...")
 		buildParams, err = defaultBuildParams()
 		if err != nil {
 			return err
@@ -638,7 +639,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 
 	// Create the magic file to indicate that this build
 	// has already been ran before!
-	file, err := options.Filesystem.Create(MagicFile)
+	file, err := opts.Filesystem.Create(MagicFile)
 	if err != nil {
 		return fmt.Errorf("create magic file: %w", err)
 	}
@@ -664,10 +665,10 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 		if err != nil {
 			return fmt.Errorf("unmarshal metadata: %w", err)
 		}
-		options.Logger(log.LevelInfo, "#3: 👀 Found devcontainer.json label metadata in image...")
+		opts.Logger(log.LevelInfo, "#3: 👀 Found devcontainer.json label metadata in image...")
 		for _, container := range devContainer {
 			if container.RemoteUser != "" {
-				options.Logger(log.LevelInfo, "#3: 🧑 Updating the user to %q!", container.RemoteUser)
+				opts.Logger(log.LevelInfo, "#3: 🧑 Updating the user to %q!", container.RemoteUser)
 
 				configFile.Config.User = container.RemoteUser
 			}
@@ -688,11 +689,11 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 		}
 	}
 
-	// Sanitize the environment of any options!
-	unsetOptionsEnv()
+	// Sanitize the environment of any opts!
+	options.UnsetEnv()
 
 	// Remove the Docker config secret file!
-	if options.DockerConfigBase64 != "" {
+	if opts.DockerConfigBase64 != "" {
 		c := filepath.Join(MagicDir, "config.json")
 		err = os.Remove(c)
 		if err != nil {
@@ -741,7 +742,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 		}
 		sort.Strings(envKeys)
 		for _, envVar := range envKeys {
-			value := devcontainer.SubstituteVars(env[envVar], options.WorkspaceFolder, os.LookupEnv)
+			value := devcontainer.SubstituteVars(env[envVar], opts.WorkspaceFolder, os.LookupEnv)
 			os.Setenv(envVar, value)
 		}
 	}
@@ -751,10 +752,10 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 	// in the export. We should have generated a complete set of environment
 	// on the intial build, so exporting environment variables a second time
 	// isn't useful anyway.
-	if options.ExportEnvFile != "" && !skippedRebuild {
-		exportEnvFile, err := os.Create(options.ExportEnvFile)
+	if opts.ExportEnvFile != "" && !skippedRebuild {
+		exportEnvFile, err := os.Create(opts.ExportEnvFile)
 		if err != nil {
-			return fmt.Errorf("failed to open EXPORT_ENV_FILE %q: %w", options.ExportEnvFile, err)
+			return fmt.Errorf("failed to open EXPORT_ENV_FILE %q: %w", opts.ExportEnvFile, err)
 		}
 
 		envKeys := make([]string, 0, len(allEnvKeys))
@@ -774,7 +775,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 		username = buildParams.User
 	}
 	if username == "" {
-		options.Logger(log.LevelWarn, "#3: no user specified, using root")
+		opts.Logger(log.LevelWarn, "#3: no user specified, using root")
 	}
 
 	userInfo, err := getUser(username)
@@ -791,13 +792,13 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 		//
 		// We need to change the ownership of the files to the user that will
 		// be running the init script.
-		if chownErr := filepath.Walk(options.WorkspaceFolder, func(path string, _ os.FileInfo, err error) error {
+		if chownErr := filepath.Walk(opts.WorkspaceFolder, func(path string, _ os.FileInfo, err error) error {
 			if err != nil {
 				return err
 			}
 			return os.Chown(path, userInfo.uid, userInfo.gid)
 		}); chownErr != nil {
-			options.Logger(log.LevelError, "chown %q: %s", userInfo.user.HomeDir, chownErr.Error())
+			opts.Logger(log.LevelError, "chown %q: %s", userInfo.user.HomeDir, chownErr.Error())
 			endStage("⚠️ Failed to the ownership of the workspace, you may need to fix this manually!")
 		} else {
 			endStage("👤 Updated the ownership of the workspace!")
@@ -814,18 +815,18 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 			}
 			return os.Chown(path, userInfo.uid, userInfo.gid)
 		}); chownErr != nil {
-			options.Logger(log.LevelError, "chown %q: %s", userInfo.user.HomeDir, chownErr.Error())
+			opts.Logger(log.LevelError, "chown %q: %s", userInfo.user.HomeDir, chownErr.Error())
 			endStage("⚠️ Failed to update ownership of %s, you may need to fix this manually!", userInfo.user.HomeDir)
 		} else {
 			endStage("🏡 Updated ownership of %s!", userInfo.user.HomeDir)
 		}
 	}
 
-	err = os.MkdirAll(options.WorkspaceFolder, 0o755)
+	err = os.MkdirAll(opts.WorkspaceFolder, 0o755)
 	if err != nil {
 		return fmt.Errorf("create workspace folder: %w", err)
 	}
-	err = os.Chdir(options.WorkspaceFolder)
+	err = os.Chdir(opts.WorkspaceFolder)
 	if err != nil {
 		return fmt.Errorf("change directory: %w", err)
 	}
@@ -837,7 +838,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 	// exec systemd as the init command, but that doesn't mean we should
 	// run the lifecycle scripts as root.
 	os.Setenv("HOME", userInfo.user.HomeDir)
-	if err := execLifecycleScripts(ctx, options, scripts, skippedRebuild, userInfo); err != nil {
+	if err := execLifecycleScripts(ctx, opts, scripts, skippedRebuild, userInfo); err != nil {
 		return err
 	}
 
@@ -846,11 +847,11 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 	//
 	// This is useful for hooking into the environment for a specific
 	// init to PID 1.
-	if options.SetupScript != "" {
+	if opts.SetupScript != "" {
 		// We execute the initialize script as the root user!
 		os.Setenv("HOME", "/root")
 
-		options.Logger(log.LevelInfo, "=== Running the setup command %q as the root user...", options.SetupScript)
+		opts.Logger(log.LevelInfo, "=== Running the setup command %q as the root user...", opts.SetupScript)
 
 		envKey := "ENVBUILDER_ENV"
 		envFile := filepath.Join("/", MagicDir, "environ")
@@ -860,12 +861,12 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 		}
 		_ = file.Close()
 
-		cmd := exec.CommandContext(ctx, "/bin/sh", "-c", options.SetupScript)
+		cmd := exec.CommandContext(ctx, "/bin/sh", "-c", opts.SetupScript)
 		cmd.Env = append(os.Environ(),
 			fmt.Sprintf("%s=%s", envKey, envFile),
 			fmt.Sprintf("TARGET_USER=%s", userInfo.user.Username),
 		)
-		cmd.Dir = options.WorkspaceFolder
+		cmd.Dir = opts.WorkspaceFolder
 		// This allows for a really nice and clean experience to experiement with!
 		// e.g. docker run --it --rm -e INIT_SCRIPT bash ...
 		if isatty.IsTerminal(os.Stdout.Fd()) && isatty.IsTerminal(os.Stdin.Fd()) {
@@ -877,7 +878,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 			go func() {
 				scanner := bufio.NewScanner(&buf)
 				for scanner.Scan() {
-					options.Logger(log.LevelInfo, "%s", scanner.Text())
+					opts.Logger(log.LevelInfo, "%s", scanner.Text())
 				}
 			}()
 
@@ -907,7 +908,7 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 			key := pair[0]
 			switch key {
 			case "INIT_COMMAND":
-				options.InitCommand = pair[1]
+				opts.InitCommand = pair[1]
 				updatedCommand = true
 			case "INIT_ARGS":
 				initArgs, err = shellquote.Split(pair[1])
@@ -943,9 +944,9 @@ ENTRYPOINT [%q]`, exePath, exePath, exePath)
 		return fmt.Errorf("set uid: %w", err)
 	}
 
-	options.Logger(log.LevelInfo, "=== Running the init command %s %+v as the %q user...", options.InitCommand, initArgs, userInfo.user.Username)
+	opts.Logger(log.LevelInfo, "=== Running the init command %s %+v as the %q user...", opts.InitCommand, initArgs, userInfo.user.Username)
 
-	err = syscall.Exec(options.InitCommand, append([]string{options.InitCommand}, initArgs...), os.Environ())
+	err = syscall.Exec(opts.InitCommand, append([]string{opts.InitCommand}, initArgs...), os.Environ())
 	if err != nil {
 		return fmt.Errorf("exec init script: %w", err)
 	}
@@ -1039,7 +1040,7 @@ func execOneLifecycleScript(
 
 func execLifecycleScripts(
 	ctx context.Context,
-	options Options,
+	options options.Options,
 	scripts devcontainer.LifecycleScripts,
 	skippedRebuild bool,
 	userInfo userInfo,
@@ -1093,25 +1094,6 @@ func createPostStartScript(path string, postStartCommand devcontainer.LifecycleS
 	return nil
 }
 
-// unsetOptionsEnv unsets all environment variables that are used
-// to configure the options.
-func unsetOptionsEnv() {
-	var o Options
-	for _, opt := range o.CLI() {
-		if opt.Env == "" {
-			continue
-		}
-		// Do not strip options that do not have the magic prefix!
-		// For example, CODER_AGENT_URL, CODER_AGENT_TOKEN, CODER_AGENT_SUBSYSTEM.
-		if !strings.HasPrefix(opt.Env, envPrefix) {
-			continue
-		}
-		// Strip both with and without prefix.
-		os.Unsetenv(opt.Env)
-		os.Unsetenv(strings.TrimPrefix(opt.Env, envPrefix))
-	}
-}
-
 func newColor(value ...color.Attribute) *color.Color {
 	c := color.New(value...)
 	c.EnableColor()
@@ -1126,7 +1108,7 @@ func (fs *osfsWithChmod) Chmod(name string, mode os.FileMode) error {
 	return os.Chmod(name, mode)
 }
 
-func findDevcontainerJSON(options Options) (string, string, error) {
+func findDevcontainerJSON(options options.Options) (string, string, error) {
 	// 0. Check if custom devcontainer directory or path is provided.
 	if options.DevcontainerDir != "" || options.DevcontainerJSONPath != "" {
 		devcontainerDir := options.DevcontainerDir
diff --git a/git.go b/git.go
index f28cab8..eb75b65 100644
--- a/git.go
+++ b/git.go
@@ -9,6 +9,8 @@ import (
 	"os"
 	"strings"
 
+	"github.com/coder/envbuilder/options"
+
 	giturls "github.com/chainguard-dev/git-urls"
 	"github.com/coder/envbuilder/internal/log"
 	"github.com/go-git/go-billy/v5"
@@ -177,7 +179,7 @@ func LogHostKeyCallback(logger log.Func) gossh.HostKeyCallback {
 // If SSH_KNOWN_HOSTS is not set, the SSH auth method will be configured
 // to accept and log all host keys. Otherwise, host key checking will be
 // performed as usual.
-func SetupRepoAuth(options *Options) transport.AuthMethod {
+func SetupRepoAuth(options *options.Options) transport.AuthMethod {
 	if options.GitURL == "" {
 		options.Logger(log.LevelInfo, "#1: ❔ No Git URL supplied!")
 		return nil
diff --git a/options.go b/options/options.go
similarity index 97%
rename from options.go
rename to options/options.go
index 76eddb6..dd5ee8b 100644
--- a/options.go
+++ b/options/options.go
@@ -1,6 +1,7 @@
-package envbuilder
+package options
 
 import (
+	"os"
 	"strings"
 
 	"github.com/coder/envbuilder/internal/log"
@@ -493,3 +494,22 @@ func skipDeprecatedOptions(options []serpent.Option) []serpent.Option {
 
 	return activeOptions
 }
+
+// UnsetEnv unsets all environment variables that are used
+// to configure the options.
+func UnsetEnv() {
+	var o Options
+	for _, opt := range o.CLI() {
+		if opt.Env == "" {
+			continue
+		}
+		// Do not strip options that do not have the magic prefix!
+		// For example, CODER_AGENT_URL, CODER_AGENT_TOKEN, CODER_AGENT_SUBSYSTEM.
+		if !strings.HasPrefix(opt.Env, envPrefix) {
+			continue
+		}
+		// Strip both with and without prefix.
+		_ = os.Unsetenv(opt.Env)
+		_ = os.Unsetenv(strings.TrimPrefix(opt.Env, envPrefix))
+	}
+}
diff --git a/testdata/options.golden b/options/testdata/options.golden
similarity index 100%
rename from testdata/options.golden
rename to options/testdata/options.golden
diff --git a/scripts/docsgen/main.go b/scripts/docsgen/main.go
index c79995c..83d992c 100644
--- a/scripts/docsgen/main.go
+++ b/scripts/docsgen/main.go
@@ -5,7 +5,7 @@ import (
 	"os"
 	"strings"
 
-	"github.com/coder/envbuilder"
+	"github.com/coder/envbuilder/options"
 )
 
 const (
@@ -26,7 +26,7 @@ func main() {
 		panic("start or end section comments not found in the file.")
 	}
 
-	var options envbuilder.Options
+	var options options.Options
 	mkd := "\n## Environment Variables\n\n" + options.Markdown()
 	modifiedContent := readmeContent[:startIndex+len(startSection)] + mkd + readmeContent[endIndex:]
 


Test patch:
diff --git a/envbuilder_internal_test.go b/envbuilder_internal_test.go
index 65edb9c..3af4b5e 100644
--- a/envbuilder_internal_test.go
+++ b/envbuilder_internal_test.go
@@ -3,6 +3,8 @@ package envbuilder
 import (
 	"testing"
 
+	"github.com/coder/envbuilder/options"
+
 	"github.com/go-git/go-billy/v5/memfs"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -18,7 +20,7 @@ func TestFindDevcontainerJSON(t *testing.T) {
 		fs := memfs.New()
 
 		// when
-		_, _, err := findDevcontainerJSON(Options{
+		_, _, err := findDevcontainerJSON(options.Options{
 			Filesystem:      fs,
 			WorkspaceFolder: "/workspace",
 		})
@@ -36,7 +38,7 @@ func TestFindDevcontainerJSON(t *testing.T) {
 		require.NoError(t, err)
 
 		// when
-		_, _, err = findDevcontainerJSON(Options{
+		_, _, err = findDevcontainerJSON(options.Options{
 			Filesystem:      fs,
 			WorkspaceFolder: "/workspace",
 		})
@@ -56,7 +58,7 @@ func TestFindDevcontainerJSON(t *testing.T) {
 		require.NoError(t, err)
 
 		// when
-		devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{
+		devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options.Options{
 			Filesystem:      fs,
 			WorkspaceFolder: "/workspace",
 		})
@@ -78,7 +80,7 @@ func TestFindDevcontainerJSON(t *testing.T) {
 		require.NoError(t, err)
 
 		// when
-		devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{
+		devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options.Options{
 			Filesystem:      fs,
 			WorkspaceFolder: "/workspace",
 			DevcontainerDir: "experimental-devcontainer",
@@ -101,7 +103,7 @@ func TestFindDevcontainerJSON(t *testing.T) {
 		require.NoError(t, err)
 
 		// when
-		devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{
+		devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options.Options{
 			Filesystem:           fs,
 			WorkspaceFolder:      "/workspace",
 			DevcontainerJSONPath: "experimental.json",
@@ -124,7 +126,7 @@ func TestFindDevcontainerJSON(t *testing.T) {
 		require.NoError(t, err)
 
 		// when
-		devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{
+		devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options.Options{
 			Filesystem:      fs,
 			WorkspaceFolder: "/workspace",
 		})
@@ -146,7 +148,7 @@ func TestFindDevcontainerJSON(t *testing.T) {
 		require.NoError(t, err)
 
 		// when
-		devcontainerPath, devcontainerDir, err := findDevcontainerJSON(Options{
+		devcontainerPath, devcontainerDir, err := findDevcontainerJSON(options.Options{
 			Filesystem:      fs,
 			WorkspaceFolder: "/workspace",
 		})
diff --git a/git_test.go b/git_test.go
index 38efee1..842cf2c 100644
--- a/git_test.go
+++ b/git_test.go
@@ -12,6 +12,8 @@ import (
 	"regexp"
 	"testing"
 
+	"github.com/coder/envbuilder/options"
+
 	"github.com/coder/envbuilder"
 	"github.com/coder/envbuilder/internal/log"
 	"github.com/coder/envbuilder/testutil/gittest"
@@ -265,7 +267,7 @@ func TestCloneRepoSSH(t *testing.T) {
 func TestSetupRepoAuth(t *testing.T) {
 	t.Setenv("SSH_AUTH_SOCK", "")
 	t.Run("Empty", func(t *testing.T) {
-		opts := &envbuilder.Options{
+		opts := &options.Options{
 			Logger: testLog(t),
 		}
 		auth := envbuilder.SetupRepoAuth(opts)
@@ -273,7 +275,7 @@ func TestSetupRepoAuth(t *testing.T) {
 	})
 
 	t.Run("HTTP/NoAuth", func(t *testing.T) {
-		opts := &envbuilder.Options{
+		opts := &options.Options{
 			GitURL: "http://host.tld/repo",
 			Logger: testLog(t),
 		}
@@ -282,7 +284,7 @@ func TestSetupRepoAuth(t *testing.T) {
 	})
 
 	t.Run("HTTP/BasicAuth", func(t *testing.T) {
-		opts := &envbuilder.Options{
+		opts := &options.Options{
 			GitURL:      "http://host.tld/repo",
 			GitUsername: "user",
 			GitPassword: "pass",
@@ -296,7 +298,7 @@ func TestSetupRepoAuth(t *testing.T) {
 	})
 
 	t.Run("HTTPS/BasicAuth", func(t *testing.T) {
-		opts := &envbuilder.Options{
+		opts := &options.Options{
 			GitURL:      "https://host.tld/repo",
 			GitUsername: "user",
 			GitPassword: "pass",
@@ -311,7 +313,7 @@ func TestSetupRepoAuth(t *testing.T) {
 
 	t.Run("SSH/WithScheme", func(t *testing.T) {
 		kPath := writeTestPrivateKey(t)
-		opts := &envbuilder.Options{
+		opts := &options.Options{
 			GitURL:               "ssh://host.tld/repo",
 			GitSSHPrivateKeyPath: kPath,
 			Logger:               testLog(t),
@@ -323,7 +325,7 @@ func TestSetupRepoAuth(t *testing.T) {
 
 	t.Run("SSH/NoScheme", func(t *testing.T) {
 		kPath := writeTestPrivateKey(t)
-		opts := &envbuilder.Options{
+		opts := &options.Options{
 			GitURL:               "git@host.tld:repo/path",
 			GitSSHPrivateKeyPath: kPath,
 			Logger:               testLog(t),
@@ -336,7 +338,7 @@ func TestSetupRepoAuth(t *testing.T) {
 	t.Run("SSH/OtherScheme", func(t *testing.T) {
 		// Anything that is not https:// or http:// is treated as SSH.
 		kPath := writeTestPrivateKey(t)
-		opts := &envbuilder.Options{
+		opts := &options.Options{
 			GitURL:               "git://git@host.tld:repo/path",
 			GitSSHPrivateKeyPath: kPath,
 			Logger:               testLog(t),
@@ -348,7 +350,7 @@ func TestSetupRepoAuth(t *testing.T) {
 
 	t.Run("SSH/GitUsername", func(t *testing.T) {
 		kPath := writeTestPrivateKey(t)
-		opts := &envbuilder.Options{
+		opts := &options.Options{
 			GitURL:               "host.tld:12345/repo/path",
 			GitSSHPrivateKeyPath: kPath,
 			GitUsername:          "user",
@@ -361,7 +363,7 @@ func TestSetupRepoAuth(t *testing.T) {
 
 	t.Run("SSH/PrivateKey", func(t *testing.T) {
 		kPath := writeTestPrivateKey(t)
-		opts := &envbuilder.Options{
+		opts := &options.Options{
 			GitURL:               "ssh://git@host.tld:repo/path",
 			GitSSHPrivateKeyPath: kPath,
 			Logger:               testLog(t),
@@ -376,7 +378,7 @@ func TestSetupRepoAuth(t *testing.T) {
 	})
 
 	t.Run("SSH/NoAuthMethods", func(t *testing.T) {
-		opts := &envbuilder.Options{
+		opts := &options.Options{
 			GitURL: "ssh://git@host.tld:repo/path",
 			Logger: testLog(t),
 		}
diff --git a/integration/integration_test.go b/integration/integration_test.go
index 2972357..e62bac0 100644
--- a/integration/integration_test.go
+++ b/integration/integration_test.go
@@ -21,11 +21,14 @@ import (
 	"testing"
 	"time"
 
+	"github.com/coder/envbuilder/options"
+
 	"github.com/coder/envbuilder"
 	"github.com/coder/envbuilder/devcontainer/features"
 	"github.com/coder/envbuilder/testutil/gittest"
 	"github.com/coder/envbuilder/testutil/mwtest"
 	"github.com/coder/envbuilder/testutil/registrytest"
+
 	clitypes "github.com/docker/cli/cli/config/types"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
@@ -71,7 +74,7 @@ func TestInitScriptInitCommand(t *testing.T) {
 			"Dockerfile": fmt.Sprintf("FROM %s\nRUN unlink /bin/sh", testImageAlpine),
 		},
 	})
-	_, err := runEnvbuilder(t, options{env: []string{
+	_, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		envbuilderEnv("INIT_SCRIPT", fmt.Sprintf(`wget -O - %q`, initSrv.URL)),
@@ -85,7 +88,7 @@ func TestInitScriptInitCommand(t *testing.T) {
 	}
 	require.NoError(t, ctx.Err(), "init script did not execute for prefixed env vars")
 
-	_, err = runEnvbuilder(t, options{env: []string{
+	_, err = runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		fmt.Sprintf(`INIT_SCRIPT=wget -O - %q`, initSrv.URL),
@@ -129,7 +132,7 @@ RUN printf "%%s\n" \
 				"Dockerfile": dockerFile,
 			},
 		})
-		_, err := runEnvbuilder(t, options{env: []string{
+		_, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		}})
@@ -160,7 +163,7 @@ RUN mkdir -p /myapp/somedir \
 				"Dockerfile": dockerFile,
 			},
 		})
-		_, err := runEnvbuilder(t, options{env: []string{
+		_, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		}})
@@ -178,7 +181,7 @@ func TestForceSafe(t *testing.T) {
 				"Dockerfile": "FROM " + testImageAlpine,
 			},
 		})
-		_, err := runEnvbuilder(t, options{env: []string{
+		_, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			"KANIKO_DIR=/not/envbuilder",
 			envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
@@ -194,7 +197,7 @@ func TestForceSafe(t *testing.T) {
 				"Dockerfile": "FROM " + testImageAlpine,
 			},
 		})
-		_, err := runEnvbuilder(t, options{env: []string{
+		_, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			"KANIKO_DIR=/not/envbuilder",
 			envbuilderEnv("FORCE_SAFE", "true"),
@@ -213,7 +216,7 @@ func TestFailsGitAuth(t *testing.T) {
 		username: "kyle",
 		password: "testing",
 	})
-	_, err := runEnvbuilder(t, options{env: []string{
+	_, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 	}})
 	require.ErrorContains(t, err, "authentication required")
@@ -228,7 +231,7 @@ func TestSucceedsGitAuth(t *testing.T) {
 		username: "kyle",
 		password: "testing",
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		envbuilderEnv("GIT_USERNAME", "kyle"),
@@ -252,7 +255,7 @@ func TestSucceedsGitAuthInURL(t *testing.T) {
 	u, err := url.Parse(srv.URL)
 	require.NoError(t, err)
 	u.User = url.UserPassword("kyle", "testing")
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", u.String()),
 		envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 	}})
@@ -330,7 +333,7 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) {
 			".devcontainer/feature3/install.sh":                "echo $GRAPE > /test3output",
 		},
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 	}})
 	require.NoError(t, err)
@@ -352,7 +355,7 @@ func TestBuildFromDockerfile(t *testing.T) {
 			"Dockerfile": "FROM " + testImageAlpine,
 		},
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		envbuilderEnv("DOCKER_CONFIG_BASE64", base64.StdEncoding.EncodeToString([]byte(`{"experimental": "enabled"}`))),
@@ -374,7 +377,7 @@ func TestBuildPrintBuildOutput(t *testing.T) {
 			"Dockerfile": "FROM " + testImageAlpine + "\nRUN echo hello",
 		},
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 	}})
@@ -408,7 +411,7 @@ func TestBuildIgnoreVarRunSecrets(t *testing.T) {
 	require.NoError(t, err)
 
 	t.Run("ReadWrite", func(t *testing.T) {
-		ctr, err := runEnvbuilder(t, options{
+		ctr, err := runEnvbuilder(t, runOpts{
 			env: []string{
 				envbuilderEnv("GIT_URL", srv.URL),
 				envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
@@ -422,7 +425,7 @@ func TestBuildIgnoreVarRunSecrets(t *testing.T) {
 	})
 
 	t.Run("ReadOnly", func(t *testing.T) {
-		ctr, err := runEnvbuilder(t, options{
+		ctr, err := runEnvbuilder(t, runOpts{
 			env: []string{
 				envbuilderEnv("GIT_URL", srv.URL),
 				envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
@@ -443,7 +446,7 @@ func TestBuildWithSetupScript(t *testing.T) {
 			"Dockerfile": "FROM " + testImageAlpine,
 		},
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		envbuilderEnv("SETUP_SCRIPT", "echo \"INIT_ARGS=-c 'echo hi > /wow && sleep infinity'\" >> $ENVBUILDER_ENV"),
@@ -469,7 +472,7 @@ func TestBuildFromDevcontainerInCustomPath(t *testing.T) {
 			".devcontainer/custom/Dockerfile": "FROM " + testImageUbuntu,
 		},
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		envbuilderEnv("DEVCONTAINER_DIR", ".devcontainer/custom"),
 	}})
@@ -494,7 +497,7 @@ func TestBuildFromDevcontainerInSubfolder(t *testing.T) {
 			".devcontainer/subfolder/Dockerfile": "FROM " + testImageUbuntu,
 		},
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 	}})
 	require.NoError(t, err)
@@ -518,7 +521,7 @@ func TestBuildFromDevcontainerInRoot(t *testing.T) {
 			"Dockerfile": "FROM " + testImageUbuntu,
 		},
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 	}})
 	require.NoError(t, err)
@@ -534,7 +537,7 @@ func TestBuildCustomCertificates(t *testing.T) {
 		},
 		tls: true,
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		envbuilderEnv("SSL_CERT_BASE64", base64.StdEncoding.EncodeToString(pem.EncodeToMemory(&pem.Block{
@@ -555,7 +558,7 @@ func TestBuildStopStartCached(t *testing.T) {
 			"Dockerfile": "FROM " + testImageAlpine,
 		},
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		envbuilderEnv("SKIP_REBUILD", "true"),
@@ -586,7 +589,7 @@ func TestCloneFailsFallback(t *testing.T) {
 	t.Parallel()
 	t.Run("BadRepo", func(t *testing.T) {
 		t.Parallel()
-		_, err := runEnvbuilder(t, options{env: []string{
+		_, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", "bad-value"),
 		}})
 		require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error())
@@ -603,7 +606,7 @@ func TestBuildFailsFallback(t *testing.T) {
 				"Dockerfile": "bad syntax",
 			},
 		})
-		_, err := runEnvbuilder(t, options{env: []string{
+		_, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		}})
@@ -619,7 +622,7 @@ func TestBuildFailsFallback(t *testing.T) {
 RUN exit 1`,
 			},
 		})
-		_, err := runEnvbuilder(t, options{env: []string{
+		_, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		}})
@@ -633,7 +636,7 @@ RUN exit 1`,
 				".devcontainer/devcontainer.json": "not json",
 			},
 		})
-		_, err := runEnvbuilder(t, options{env: []string{
+		_, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 		}})
 		require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error())
@@ -645,7 +648,7 @@ RUN exit 1`,
 				".devcontainer/devcontainer.json": "{}",
 			},
 		})
-		ctr, err := runEnvbuilder(t, options{env: []string{
+		ctr, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("FALLBACK_IMAGE", testImageAlpine),
 		}})
@@ -663,7 +666,7 @@ func TestExitBuildOnFailure(t *testing.T) {
 			"Dockerfile": "bad syntax",
 		},
 	})
-	_, err := runEnvbuilder(t, options{env: []string{
+	_, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		envbuilderEnv("FALLBACK_IMAGE", testImageAlpine),
@@ -697,7 +700,7 @@ func TestContainerEnv(t *testing.T) {
 			".devcontainer/Dockerfile": "FROM " + testImageAlpine + "\nENV FROM_DOCKERFILE=foo",
 		},
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		envbuilderEnv("EXPORT_ENV_FILE", "/env"),
 	}})
@@ -730,7 +733,7 @@ func TestUnsetOptionsEnv(t *testing.T) {
 			".devcontainer/Dockerfile": "FROM " + testImageAlpine + "\nENV FROM_DOCKERFILE=foo",
 		},
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		"GIT_URL", srv.URL,
 		envbuilderEnv("GIT_PASSWORD", "supersecret"),
@@ -741,13 +744,13 @@ func TestUnsetOptionsEnv(t *testing.T) {
 	require.NoError(t, err)
 
 	output := execContainer(t, ctr, "cat /root/env.txt")
-	var os envbuilder.Options
+	var os options.Options
 	for _, s := range strings.Split(strings.TrimSpace(output), "\n") {
 		for _, o := range os.CLI() {
 			if strings.HasPrefix(s, o.Env) {
 				assert.Fail(t, "environment variable should be stripped when running init script", s)
 			}
-			optWithoutPrefix := strings.TrimPrefix(o.Env, envbuilder.WithEnvPrefix(""))
+			optWithoutPrefix := strings.TrimPrefix(o.Env, options.WithEnvPrefix(""))
 			if strings.HasPrefix(s, optWithoutPrefix) {
 				assert.Fail(t, "environment variable should be stripped when running init script", s)
 			}
@@ -777,7 +780,7 @@ func TestLifecycleScripts(t *testing.T) {
 			".devcontainer/Dockerfile": "FROM " + testImageAlpine + "\nUSER nobody",
 		},
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 	}})
 	require.NoError(t, err)
@@ -816,7 +819,7 @@ RUN chmod +x /bin/init.sh
 USER nobody`,
 		},
 	})
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 		envbuilderEnv("POST_START_SCRIPT_PATH", "/tmp/post-start.sh"),
 		envbuilderEnv("INIT_COMMAND", "/bin/init.sh"),
@@ -850,7 +853,7 @@ func TestPrivateRegistry(t *testing.T) {
 				"Dockerfile": "FROM " + image,
 			},
 		})
-		_, err := runEnvbuilder(t, options{env: []string{
+		_, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 		}})
@@ -879,7 +882,7 @@ func TestPrivateRegistry(t *testing.T) {
 		})
 		require.NoError(t, err)
 
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 			envbuilderEnv("DOCKER_CONFIG_BASE64", base64.StdEncoding.EncodeToString(config)),
@@ -911,7 +914,7 @@ func TestPrivateRegistry(t *testing.T) {
 		})
 		require.NoError(t, err)
 
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
 			envbuilderEnv("DOCKER_CONFIG_BASE64", base64.StdEncoding.EncodeToString(config)),
@@ -968,7 +971,7 @@ func setupPassthroughRegistry(t *testing.T, image string, opts *setupPassthrough
 }
 
 func TestNoMethodFails(t *testing.T) {
-	_, err := runEnvbuilder(t, options{env: []string{}})
+	_, err := runEnvbuilder(t, runOpts{env: []string{}})
 	require.ErrorContains(t, err, envbuilder.ErrNoFallbackImage.Error())
 }
 
@@ -1042,7 +1045,7 @@ COPY %s .`, testImageAlpine, inclFile)
 			srv := createGitServer(t, gitServerOptions{
 				files: tc.files,
 			})
-			_, err := runEnvbuilder(t, options{env: []string{
+			_, err := runEnvbuilder(t, runOpts{env: []string{
 				envbuilderEnv("GIT_URL", srv.URL),
 				envbuilderEnv("DOCKERFILE_PATH", tc.dockerfilePath),
 				envbuilderEnv("BUILD_CONTEXT_PATH", tc.buildContextPath),
@@ -1090,7 +1093,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
 
 		// When: we run envbuilder with GET_CACHED_IMAGE
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("GET_CACHED_IMAGE", "1"),
@@ -1101,7 +1104,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
 
 		// When: we run envbuilder with PUSH_IMAGE set
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 		}})
@@ -1112,7 +1115,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		require.ErrorContains(t, err, "MANIFEST_UNKNOWN", "expected image to not be present before build + push")
 
 		// Then: re-running envbuilder with GET_CACHED_IMAGE should succeed
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("GET_CACHED_IMAGE", "1"),
@@ -1150,7 +1153,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
 
 		// When: we run envbuilder with GET_CACHED_IMAGE
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("GET_CACHED_IMAGE", "1"),
@@ -1161,7 +1164,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
 
 		// When: we run envbuilder with PUSH_IMAGE set
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("PUSH_IMAGE", "1"),
@@ -1184,7 +1187,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		}
 
 		// Then: re-running envbuilder with GET_CACHED_IMAGE should succeed
-		ctrID, err := runEnvbuilder(t, options{env: []string{
+		ctrID, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("GET_CACHED_IMAGE", "1"),
@@ -1283,7 +1286,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
 
 		// When: we run envbuilder with GET_CACHED_IMAGE
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("GET_CACHED_IMAGE", "1"),
@@ -1294,7 +1297,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
 
 		// When: we run envbuilder with PUSH_IMAGE set
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("PUSH_IMAGE", "1"),
@@ -1307,7 +1310,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		require.NoError(t, err, "expected image to be present after build + push")
 
 		// Then: re-running envbuilder with GET_CACHED_IMAGE should succeed
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("GET_CACHED_IMAGE", "1"),
@@ -1351,7 +1354,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
 
 		// When: we run envbuilder with GET_CACHED_IMAGE
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("GET_CACHED_IMAGE", "1"),
@@ -1362,7 +1365,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
 
 		// When: we run envbuilder with PUSH_IMAGE set
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("PUSH_IMAGE", "1"),
@@ -1404,7 +1407,7 @@ COPY --from=a /root/date.txt /date.txt`, testImageAlpine, testImageAlpine),
 		require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
 
 		// When: we run envbuilder with GET_CACHED_IMAGE
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("GET_CACHED_IMAGE", "1"),
@@ -1416,7 +1419,7 @@ COPY --from=a /root/date.txt /date.txt`, testImageAlpine, testImageAlpine),
 		require.ErrorContains(t, err, "NAME_UNKNOWN", "expected image to not be present before build + push")
 
 		// When: we run envbuilder with PUSH_IMAGE set
-		ctrID, err := runEnvbuilder(t, options{env: []string{
+		ctrID, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("PUSH_IMAGE", "1"),
@@ -1432,7 +1435,7 @@ COPY --from=a /root/date.txt /date.txt`, testImageAlpine, testImageAlpine),
 		require.NoError(t, err, "expected image to be present after build + push")
 
 		// Then: re-running envbuilder with GET_CACHED_IMAGE should succeed
-		_, err = runEnvbuilder(t, options{env: []string{
+		_, err = runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", testRepo),
 			envbuilderEnv("GET_CACHED_IMAGE", "1"),
@@ -1463,7 +1466,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		})
 
 		// When: we run envbuilder with PUSH_IMAGE set but no cache repo set
-		_, err := runEnvbuilder(t, options{env: []string{
+		_, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("PUSH_IMAGE", "1"),
 		}})
@@ -1499,7 +1502,7 @@ RUN date --utc > /root/date.txt`, testImageAlpine),
 		notRegURL := strings.TrimPrefix(notRegSrv.URL, "http://") + "/test"
 
 		// When: we run envbuilder with PUSH_IMAGE set
-		_, err := runEnvbuilder(t, options{env: []string{
+		_, err := runEnvbuilder(t, runOpts{env: []string{
 			envbuilderEnv("GIT_URL", srv.URL),
 			envbuilderEnv("CACHE_REPO", notRegURL),
 			envbuilderEnv("PUSH_IMAGE", "1"),
@@ -1535,7 +1538,7 @@ USER test
 
 	// Run envbuilder with a Docker volume mounted to homedir
 	volName := fmt.Sprintf("%s%d-home", t.Name(), time.Now().Unix())
-	ctr, err := runEnvbuilder(t, options{env: []string{
+	ctr, err := runEnvbuilder(t, runOpts{env: []string{
 		envbuilderEnv("GIT_URL", srv.URL),
 	}, volumes: map[string]string{volName: "/home/test"}})
 	require.NoError(t, err)
@@ -1656,7 +1659,7 @@ func cleanOldEnvbuilders() {
 	}
 }
 
-type options struct {
+type runOpts struct {
 	binds   []string
 	env     []string
 	volumes map[string]string
@@ -1664,7 +1667,7 @@ type options struct {
 
 // runEnvbuilder starts the envbuilder container with the given environment
 // variables and returns the container ID.
-func runEnvbuilder(t *testing.T, options options) (string, error) {
+func runEnvbuilder(t *testing.T, opts runOpts) (string, error) {
 	t.Helper()
 	ctx := context.Background()
 	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
@@ -1673,7 +1676,7 @@ func runEnvbuilder(t *testing.T, options options) (string, error) {
 		cli.Close()
 	})
 	mounts := make([]mount.Mount, 0)
-	for volName, volPath := range options.volumes {
+	for volName, volPath := range opts.volumes {
 		mounts = append(mounts, mount.Mount{
 			Type:   mount.TypeVolume,
 			Source: volName,
@@ -1689,13 +1692,13 @@ func runEnvbuilder(t *testing.T, options options) (string, error) {
 	}
 	ctr, err := cli.ContainerCreate(ctx, &container.Config{
 		Image: "envbuilder:latest",
-		Env:   options.env,
+		Env:   opts.env,
 		Labels: map[string]string{
 			testContainerLabel: "true",
 		},
 	}, &container.HostConfig{
 		NetworkMode: container.NetworkMode("host"),
-		Binds:       options.binds,
+		Binds:       opts.binds,
 		Mounts:      mounts,
 	}, nil, nil, "")
 	require.NoError(t, err)
@@ -1784,5 +1787,5 @@ func streamContainerLogs(t *testing.T, cli *client.Client, containerID string) (
 }
 
 func envbuilderEnv(env string, value string) string {
-	return fmt.Sprintf("%s=%s", envbuilder.WithEnvPrefix(env), value)
+	return fmt.Sprintf("%s=%s", options.WithEnvPrefix(env), value)
 }
diff --git a/options_test.go b/options/options_test.go
similarity index 88%
rename from options_test.go
rename to options/options_test.go
index e32af9e..bf7a216 100644
--- a/options_test.go
+++ b/options/options_test.go
@@ -1,4 +1,4 @@
-package envbuilder_test
+package options_test
 
 import (
 	"bytes"
@@ -6,7 +6,8 @@ import (
 	"os"
 	"testing"
 
-	"github.com/coder/envbuilder"
+	"github.com/coder/envbuilder/options"
+
 	"github.com/coder/serpent"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -16,50 +17,50 @@ import (
 func TestEnvOptionParsing(t *testing.T) {
 	t.Run("string", func(t *testing.T) {
 		const val = "setup.sh"
-		t.Setenv(envbuilder.WithEnvPrefix("SETUP_SCRIPT"), val)
+		t.Setenv(options.WithEnvPrefix("SETUP_SCRIPT"), val)
 		o := runCLI()
 		require.Equal(t, o.SetupScript, val)
 	})
 
 	t.Run("int", func(t *testing.T) {
-		t.Setenv(envbuilder.WithEnvPrefix("CACHE_TTL_DAYS"), "7")
+		t.Setenv(options.WithEnvPrefix("CACHE_TTL_DAYS"), "7")
 		o := runCLI()
 		require.Equal(t, o.CacheTTLDays, int64(7))
 	})
 
 	t.Run("string array", func(t *testing.T) {
-		t.Setenv(envbuilder.WithEnvPrefix("IGNORE_PATHS"), "/var,/temp")
+		t.Setenv(options.WithEnvPrefix("IGNORE_PATHS"), "/var,/temp")
 		o := runCLI()
 		require.Equal(t, o.IgnorePaths, []string{"/var", "/temp"})
 	})
 
 	t.Run("bool", func(t *testing.T) {
 		t.Run("lowercase", func(t *testing.T) {
-			t.Setenv(envbuilder.WithEnvPrefix("SKIP_REBUILD"), "true")
-			t.Setenv(envbuilder.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "false")
+			t.Setenv(options.WithEnvPrefix("SKIP_REBUILD"), "true")
+			t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "false")
 			o := runCLI()
 			require.True(t, o.SkipRebuild)
 			require.False(t, o.GitCloneSingleBranch)
 		})
 
 		t.Run("uppercase", func(t *testing.T) {
-			t.Setenv(envbuilder.WithEnvPrefix("SKIP_REBUILD"), "TRUE")
-			t.Setenv(envbuilder.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "FALSE")
+			t.Setenv(options.WithEnvPrefix("SKIP_REBUILD"), "TRUE")
+			t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "FALSE")
 			o := runCLI()
 			require.True(t, o.SkipRebuild)
 			require.False(t, o.GitCloneSingleBranch)
 		})
 
 		t.Run("numeric", func(t *testing.T) {
-			t.Setenv(envbuilder.WithEnvPrefix("SKIP_REBUILD"), "1")
-			t.Setenv(envbuilder.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "0")
+			t.Setenv(options.WithEnvPrefix("SKIP_REBUILD"), "1")
+			t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "0")
 			o := runCLI()
 			require.True(t, o.SkipRebuild)
 			require.False(t, o.GitCloneSingleBranch)
 		})
 
 		t.Run("empty", func(t *testing.T) {
-			t.Setenv(envbuilder.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "")
+			t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "")
 			o := runCLI()
 			require.False(t, o.GitCloneSingleBranch)
 		})
@@ -142,7 +143,7 @@ var updateCLIOutputGoldenFiles = flag.Bool("update", false, "update options CLI
 
 // TestCLIOutput tests that the default CLI output is as expected.
 func TestCLIOutput(t *testing.T) {
-	var o envbuilder.Options
+	var o options.Options
 	cmd := serpent.Command{
 		Use:     "envbuilder",
 		Options: o.CLI(),
@@ -171,8 +172,8 @@ func TestCLIOutput(t *testing.T) {
 	}
 }
 
-func runCLI() envbuilder.Options {
-	var o envbuilder.Options
+func runCLI() options.Options {
+	var o options.Options
 	cmd := serpent.Command{
 		Options: o.CLI(),
 		Handler: func(inv *serpent.Invocation) error {