Skip to content

Conversation

@ThomasK33
Copy link
Member

@ThomasK33 ThomasK33 commented Dec 10, 2025

Previously, the -y flag only bypassed the final "Confirm create?" prompt while still showing interactive prompts for preset selection, parameter input, and template selection. This broke CI/CD and automation workflows that expected -y to enable true non-interactive mode.

Now when -y is passed: workspace name must be an argument, templates auto-select if only one exists (error if multiple), presets default to "none", parameters use their defaults (error if required without default), and external auth must be pre-authenticated.

Scenario Behavior with -y
Workspace name missing Error: provide as argument
Single template available Auto-select
Multiple templates Error: use --template
Preset selection Skip (default to none)
Parameters with defaults Use default
Required param, no default Error: use --parameter
External auth required Error: pre-authenticate first
Implementation Plan

Plan: Fix -y Flag to Properly Support Non-Interactive Mode in coder create

Problem

The -y flag claims to "Bypass prompts" but only bypasses the final "Confirm create?" prompt. All other interactive prompts (preset selection, parameter input, template selection, workspace name) still appear, breaking non-interactive/CI environments.

Requirements

  1. If -y is passed, never prompt the user
  2. If no preset explicitly passed with -y, default to "none" (skip presets)
  3. If a parameter has a default value, use it automatically
  4. If a required parameter has no default value, fail with explicit error

Files to Modify

  1. cli/create.go - Main create command logic
  2. cli/parameterresolver.go - Parameter resolution with input prompts
  3. cli/cliui/externalauth.go - External auth handling
  4. cli/cliui/prompt.go - Update flag description (minor)

Implementation Plan

1. Add Helper to Check Non-Interactive Mode

In cli/create.go, add a helper function to check if we're in non-interactive mode:

func isNonInteractive(inv *serpent.Invocation) bool {
    if inv.ParsedFlags().Lookup("yes") != nil {
        if skip, _ := inv.ParsedFlags().GetBool("yes"); skip {
            return true
        }
    }
    return false
}

2. Handle Workspace Name (cli/create.go:77-95)

Current: Prompts if workspace name not provided as argument.

Change: If -y and no workspace name, return an error:

if workspaceName == "" {
    if isNonInteractive(inv) {
        return xerrors.New("workspace name is required in non-interactive mode; provide it as an argument")
    }
    // existing prompt code...
}

3. Handle Template Selection (cli/create.go:124-178)

Current: Prompts with template selection if --template not provided.

Change: If -y and no template specified, return an error:

case templateName == "":
    if isNonInteractive(inv) {
        return xerrors.New("template is required in non-interactive mode; use --template flag")
    }
    // existing prompt code...

4. Handle Preset Selection (cli/create.go:293-304)

Current: If presets exist and no --preset flag, prompts for selection.

Change: If -y and no preset specified, default to "none" (skip presets):

if len(tvPresets) > 0 && strings.ToLower(presetName) != PresetNone {
    preset, err = resolvePreset(tvPresets, presetName)
    if err != nil {
        if !errors.Is(err, ErrNoPresetFound) {
            return xerrors.Errorf("unable to resolve preset: %w", err)
        }
        // NEW: In non-interactive mode, skip presets instead of prompting
        if isNonInteractive(inv) {
            // No preset found and non-interactive - skip presets
            preset = nil
        } else {
            // Interactive mode - prompt user
            if preset, err = promptPresetSelection(inv, tvPresets); err != nil {
                return xerrors.Errorf("unable to prompt user for preset: %w", err)
            }
        }
    }
    // ... rest of preset handling
}

5. Handle Parameter Input (cli/parameterresolver.go)

Current: resolveWithInput prompts for any unresolved parameters.

Change: Pass non-interactive flag to resolver and handle appropriately.

5a. Add field to ParameterResolver struct:

type ParameterResolver struct {
    // ... existing fields
    nonInteractive bool  // NEW
}

func (pr *ParameterResolver) WithNonInteractive(nonInteractive bool) *ParameterResolver {
    pr.nonInteractive = nonInteractive
    return pr
}

5b. Modify resolveWithInput to respect non-interactive mode:

func (pr *ParameterResolver) resolveWithInput(...) ([]codersdk.WorkspaceBuildParameter, error) {
    for _, tvp := range templateVersionParameters {
        p := findWorkspaceBuildParameter(tvp.Name, resolved)
        if p != nil {
            continue
        }

        // Parameter needs resolution
        firstTimeUse := pr.isFirstTimeUse(tvp.Name)
        promptParameterOption := pr.isLastBuildParameterInvalidOption(tvp)

        needsInput := (tvp.Ephemeral && pr.promptEphemeralParameters) ||
            (action == WorkspaceCreate && tvp.Required) ||
            (action == WorkspaceCreate && !tvp.Ephemeral) ||
            (action == WorkspaceUpdate && promptParameterOption) ||
            (action == WorkspaceUpdate && tvp.Mutable && tvp.Required) ||
            (action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) ||
            (tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters)

        if needsInput {
            // NEW: In non-interactive mode, use default or fail
            if pr.nonInteractive {
                if tvp.DefaultValue != "" {
                    // Use default value
                    resolved = append(resolved, codersdk.WorkspaceBuildParameter{
                        Name:  tvp.Name,
                        Value: tvp.DefaultValue,
                    })
                } else if tvp.Required {
                    // Required parameter with no default - fail
                    return nil, xerrors.Errorf(
                        "parameter %q is required but has no default value; "+
                        "provide it with --parameter %s=<value>",
                        tvp.Name, tvp.Name)
                }
                // Optional parameter with no default - skip (will use empty/server default)
                continue
            }

            // Interactive mode - prompt as before
            parameterValue, err := cliui.RichParameter(inv, tvp, pr.richParametersDefaults)
            // ... existing code
        }
        // ... rest of existing logic
    }
    return resolved, nil
}

5c. Update caller in cli/create.go to pass non-interactive flag:

resolver := new(ParameterResolver).
    // ... existing chain
    WithNonInteractive(isNonInteractive(inv))

6. Update Flag Description

In cli/cliui/prompt.go, update the description to be accurate:

func SkipPromptOption() serpent.Option {
    return serpent.Option{
        Flag:          skipPromptFlag,
        FlagShorthand: "y",
        Description:   "Run in non-interactive mode. Use default values and fail if required inputs are missing.",
        Value:         serpent.BoolOf(new(bool)),
    }
}

7. Handle External Auth (cli/create.go:571)

Current: Waits/polls for browser-based authentication if external auth is required.

Change: In non-interactive mode, fail immediately if any required external auth is not already authenticated:

// In prepWorkspaceBuild, modify the ExternalAuth call
err = cliui.ExternalAuth(ctx, inv.Stdout, cliui.ExternalAuthOptions{
    Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionExternalAuth, error) {
        return client.TemplateVersionExternalAuth(ctx, templateVersion.ID)
    },
    NonInteractive: isNonInteractive(inv),  // NEW: pass flag
})

Modify cliui.ExternalAuth in cli/cliui/externalauth.go to accept a non-interactive flag and fail immediately if required auth is missing:

type ExternalAuthOptions struct {
    Fetch          func(ctx context.Context) ([]codersdk.TemplateVersionExternalAuth, error)
    NonInteractive bool  // NEW
}

// In the function body, after fetching auth status:
for _, auth := range auths {
    if !auth.Authenticated && !auth.Optional {
        if o.NonInteractive {
            return xerrors.Errorf("external authentication %q is required but not authenticated; "+
                "authenticate via browser first or ensure the template marks it as optional", auth.DisplayName)
        }
        // ... existing polling/waiting logic
    }
}

Edge Cases

  1. Ephemeral Parameters: These are build-time options. In non-interactive mode with --prompt-ephemeral-parameters, we should probably fail if ephemeral parameters exist but weren't provided via --ephemeral-parameter flags.

  2. Multi-select Parameters (list(string)): These have defaults that are JSON arrays. The default handling should work, but verify the default value is properly applied.

Testing

  1. Test coder create workspace -t template -y with a template that has:

    • Presets → should skip presets (default to none)
    • Required parameters with defaults → should use defaults
    • Required parameters without defaults → should fail with clear error
    • Optional parameters → should use defaults or skip
  2. Test error messages are clear and actionable

  3. Test that interactive mode (without -y) still works as before

Summary of Changes

Location Current Behavior New Behavior with -y
Workspace name Prompts Error: provide as argument
Template selection Prompts Error: use --template
Preset selection Prompts Skip presets (default to none)
Parameters with defaults Prompts Use default value
Required params no default Prompts Error: use --parameter
External auth (required) Waits/polls Error: must pre-authenticate
Confirm create Skipped ✓ Skipped ✓ (already works)

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

@bpmct
Copy link
Member

bpmct commented Dec 10, 2025

@ThomasK33 I wrote a guide for PRs which should create less AI-like descriptions. However, it seems like claude code doesn't always listen. Any ideas why? https://github.com/coder/coder/blob/main/.claude/docs/PR_STYLE_GUIDE.md

Perhaps these should be skills

@ThomasK33 ThomasK33 force-pushed the fix-create-non-interactive-y-flag branch from 9d5bb50 to 34b73d4 Compare December 10, 2025 15:36
Copy link
Member Author

I instructed CC to create the PR near the compaction window limit, around 150k tokens. As a result, its recall may no longer be optimal, since all documents have been placed at the front, and tool calls occurred in between.

If we want to optimize for Claude Code, we might consider introducing a sub-agent dedicated to strictly following rules within a much cleaner and more streamlined context window, instead of letting the main session handle the PR description.

The -y flag was documented as 'Bypass prompts' but only bypassed the final
'Confirm create?' prompt. All other interactive prompts (preset selection,
parameter input, template selection, workspace name) would still appear,
breaking CI/CD and non-interactive usage.

Changes:
- Add isNonInteractive(inv) helper in create.go
- Workspace name: error if not provided with -y
- Template selection: auto-select if exactly 1 template exists, error if multiple
- Preset selection: skip presets entirely (default to none) with -y
- Parameters: use default values automatically, error if required param has no default
- External auth: error immediately if required auth not authenticated
- Update SkipPromptOption() description to accurately describe behavior
- Regenerate golden files for updated flag description

The -y flag now enables true non-interactive mode for `coder create`.
@ThomasK33 ThomasK33 force-pushed the fix-create-non-interactive-y-flag branch from 34b73d4 to 7e2b111 Compare December 10, 2025 16:00
@ThomasK33
Copy link
Member Author

@ThomasK33 I wrote a guide for PRs which should create less AI-like descriptions. However, it seems like claude code doesn't always listen. Any ideas why? main/.claude/docs/PR_STYLE_GUIDE.md

Perhaps these should be skills

@bpmct, rerean it on a fresh context, and the PR description is now more aligned with what's in that doc.

@ThomasK33 ThomasK33 marked this pull request as ready for review December 10, 2025 16:04
// isNonInteractive checks if the command is running in non-interactive mode
// (i.e., the --yes/-y flag was provided).
func isNonInteractive(inv *serpent.Invocation) bool {
if inv.ParsedFlags().Lookup("yes") != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if that’s a pattern or not, but can we not do a double look up of string “yes”, there’s gotta be a better way to handle this

Copy link
Member

@deansheather deansheather left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I was going to suggest leaving the erroring up to cliui.Select and co., but it's probably better to deal with it in the command handler like you've done so we get better error messages.

Looks good.


// isNonInteractive checks if the command is running in non-interactive mode
// (i.e., the --yes/-y flag was provided).
func isNonInteractive(inv *serpent.Invocation) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably go into the cliui package beside the SkipPromptOption function

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants