charmbracelet/crush · error

could not build runner for %s: %w

Error message

could not build runner for %s: %w

What it means

runShellSource wraps a failure from interp.New when constructing the nested interpreter runner that executes a parsed shell file. Runner construction validates options (StdIO, Env, Dir, Params, exec handler); an invalid combination makes the runner unbuildable before any script line executes.

Source

Thrown at internal/shell/dispatch.go:408

	opts := []interp.RunnerOption{
		interp.StdIO(hc.Stdin, hc.Stdout, hc.Stderr),
		interp.Interactive(false),
		interp.Env(hc.Env),
		interp.Dir(hc.Dir),
		execHandlerOption(blockFuncs),
	}
	if len(args) > 1 {
		// Params with a leading "--" avoids any of args[1:] being
		// misinterpreted as set-options (e.g. a user passing "-e" as
		// a positional arg to their script).
		params := append([]string{"--"}, args[1:]...)
		opts = append(opts, interp.Params(params...))
	}

	runner, err := interp.New(opts...)
	if err != nil {
		return fmt.Errorf("could not build runner for %s: %w", path, err)
	}
	return runner.Run(ctx, file)
}

// execEnvList converts an expand.Environ to the []string form that
// os/exec.Cmd.Env expects. Only exported string variables are included,
// matching what a real shell would pass to a child process.
func execEnvList(env expand.Environ) []string {
	var out []string
	env.Each(func(name string, vr expand.Variable) bool {
		if vr.Exported && vr.Kind == expand.String {
			out = append(out, name+"="+vr.Str)
		}
		return true
	})
	return out
}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Inspect the wrapped mvdan.cc/sh error to identify which runner option is invalid.
  2. Verify the parent interp.HandlerCtx Dir/Env values are sane (existing directory, well-formed env list).
  3. Check that args passed via interp.Params don't include malformed set-options; the code prefixes "--" to guard this.
  4. If it persists after a mvdan.cc/sh upgrade, pin the previous version and report the incompatibility.
Defensive patterns

Strategy: try-catch

Validate before calling

if st, err := os.Stat(dir); err != nil || !st.IsDir() {
    // bad Dir option would break interp.New — fix before dispatch
}

Try / catch

if err != nil {
    if strings.HasPrefix(err.Error(), "could not build runner for ") {
        // runner construction failed; inspect wrapped interp.New error
    }
}

Prevention

When it happens

Trigger: After a successful syntax parse, interp.New(opts...) returns an error — e.g. invalid interp.Params values, an unusable interp.Dir, or a misconfigured exec handler option — while dispatching a shell script in-process.

Common situations: Programmatic misconfiguration of runner options; a parent HandlerCtx carrying a bad working directory; regression in custom execHandlerOption wiring after refactors; rare mvdan.cc/sh version incompatibilities.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/ae27899fef74ca3a. Report an issue: GitHub.