{"record":{"id":"b7930b5fd71b1983","repo":"charmbracelet/crush","slug":"command-execution-panic-v","errorCode":null,"errorMessage":"command execution panic: %v","messagePattern":"command execution panic: (.+?)","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/shell/run.go","lineNumber":64,"sourceCode":"\t// TermWidth is the terminal width in columns for PTY execution.\n\t// Zero uses a default of 200.\n\tTermWidth int\n}\n\n// Run parses and executes a shell command using the same mvdan.cc/sh\n// interpreter stack that the stateful [Shell] type uses (builtins,\n// optional block list, optional Go coreutils). It is safe to call\n// concurrently from multiple goroutines: each call builds its own\n// [interp.Runner] and shares no state with other callers or with any\n// [Shell] instance.\n//\n// Errors returned from the command itself (non-zero exit, context\n// cancellation, parse failures) follow the same conventions as\n// [Shell.Exec]: inspect with [IsInterrupt] and [ExitCode].\nfunc Run(ctx context.Context, opts RunOptions) (err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = fmt.Errorf(\"command execution panic: %v\", r)\n\t\t}\n\t}()\n\n\tif opts.Cwd == \"\" {\n\t\treturn fmt.Errorf(\"shell.Run: Cwd is required\")\n\t}\n\n\tstdout := opts.Stdout\n\tif stdout == nil {\n\t\tstdout = io.Discard\n\t}\n\tstderr := opts.Stderr\n\tif stderr == nil {\n\t\tstderr = io.Discard\n\t}\n\n\tline, err := syntax.NewParser().Parse(strings.NewReader(opts.Command), \"\")\n\tif err != nil {","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/shell/run.go#L46-L82","documentation":"shell.Run recovers any panic raised during command execution and converts it into this error instead of crashing the process. The deferred recover in Run captures the panic value r and returns fmt.Errorf(\"command execution panic: %v\", r), so callers see a normal error rather than a program crash.","triggerScenarios":"Any panic inside Run's execution path — the mvdan.cc/sh interpreter, custom builtins, BlockFunc matchers, or writer implementations passed via RunOptions panicking (e.g. nil map write, index out of range) during a registered-builtin or blocked-command run.","commonSituations":"A custom builtin handler panics on unexpected arguments; a BlockFunc dereferences nil; a bug in a PTY/writer wrapper; concurrent misuse of a non-thread-safe writer passed as Stdout.","solutions":["Inspect the panic message and stack (enable verbose logging) to find the panicking component.","If you supply custom builtins or BlockFuncs, add nil checks and argument validation so they cannot panic.","Ensure Stdin/Stdout/Stderr writers are safe for concurrent use if Run is called from multiple goroutines.","Reproduce with a minimal Command and options set, then report upstream if the panic is inside mvdan.cc/sh."],"exampleFix":"// before\nRun(ctx, RunOptions{Command: \"mybuiltin\", Cwd: \"/tmp\", BlockFuncs: []BlockFunc{nil}})\n// after\nRun(ctx, RunOptions{Command: \"mybuiltin\", Cwd: \"/tmp\", BlockFuncs: []BlockFunc{safeMatcher}})","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := shell.Run(ctx, opts); err != nil {\n    if strings.HasPrefix(err.Error(), \"command execution panic: \") {\n        log.Printf(\"shell.Run panicked: %v\", err) // plus capture stack via debug.Stack in wrappers\n    }\n}","preventionTips":["Wrap custom builtins and BlockFuncs with recover + nil checks.","Fuzz custom handlers with unexpected arguments.","Use concurrency-safe writers for Stdout/Stderr.","Pin and test mvdan.cc/sh versions in CI."],"tags":["shell","panic","recovery","runtime"],"backgroundTag":"command-execution-panic","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}