{"record":{"id":"9aa510e278e65f46","repo":"charmbracelet/crush","slug":"w-s-9aa510","errorCode":null,"errorMessage":"%w: %s","messagePattern":"%w: %s","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/shell/expand.go","lineNumber":142,"sourceCode":"\t\t\tif rerr := runner.Run(ctx, &syntax.File{Stmts: cs.Stmts}); rerr != nil {\n\t\t\t\treturn wrapCmdSubstErr(rerr, stderrBuf.Bytes())\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t\t// ReadDir / ReadDir2 left nil: globbing is disabled.\n\t}\n\n\treturn expand.Document(cfg, word)\n}\n\n// wrapCmdSubstErr attaches a bounded prefix of the inner command's stderr\n// to the original error, if any.\nfunc wrapCmdSubstErr(err error, stderrBytes []byte) error {\n\tmsg := sanitizeStderr(stderrBytes)\n\tif msg == \"\" {\n\t\treturn err\n\t}\n\treturn fmt.Errorf(\"%w: %s\", err, msg)\n}\n\n// sanitizeStderr trims, bounds, and scrubs non-printable bytes from the\n// stderr of a failing command so the result is safe to include in an\n// error message shown to the user.\nfunc sanitizeStderr(b []byte) string {\n\tb = bytes.TrimRight(b, \"\\n\")\n\tif len(b) > maxInnerStderrBytes {\n\t\tb = b[:maxInnerStderrBytes]\n\t}\n\tout := make([]byte, len(b))\n\tfor i, c := range b {\n\t\tif c == '\\t' || c == '\\n' || (c >= 0x20 && c < 0x7f) {\n\t\t\tout[i] = c\n\t\t} else {\n\t\t\tout[i] = '?'\n\t\t}\n\t}","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/shell/expand.go#L124-L160","documentation":"wrapCmdSubstErr augments a failed command substitution error with a sanitized, bounded prefix of the inner command's stderr. When a $(...) inside an expanded value fails (non-zero exit or interpreter error), the original error is wrapped via %w and the scrubbed stderr text is appended after a colon.","triggerScenarios":"ExpandValue evaluates a $(...) command substitution; the inner runner returns an error and its captured stderr is non-empty after sanitizeStderr (trim trailing newlines, bound length, replace non-printables with '?'). Empty stderr leaves the original error untouched.","commonSituations":"Config values embedding $(git rev-parse ...) or similar in repos where the command fails (not a git repo, missing binary) and prints a diagnostic to stderr; the appended text is the tool's own error message.","solutions":["Read the appended stderr text — it is the failing inner command's own diagnostic; fix the underlying command.","Verify any binaries referenced inside $(...) exist in the environment ExpandValue runs in.","Ensure commands in substitutions are safe for a non-interactive environment (no TTY assumptions).","Match on the wrapped error with errors.Is/As for exit codes (interp.ExitStatus) to distinguish exit failures from other errors."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// verify inner command works before embedding it in a config value\nif err := exec.Command(\"git\", \"rev-parse\", \"--git-dir\").Run(); err != nil {\n    // $(git ...) substitution would fail during ExpandValue\n}","typeGuard":null,"tryCatchPattern":"if err != nil {\n    var xe interp.ExitStatus\n    if errors.As(err, &xe) {\n        // substitution command exited non-zero; appended stderr has details\n    }\n}","preventionTips":["Test commands embedded in $() inside the target environment.","Keep stderr in inner commands meaningful; it is appended to the error.","Avoid substitutions dependent on TTY or interactive state.","Keep the wrapped error (errors.Is/As) for exit-code inspection."],"tags":["shell","command-substitution","stderr","error-wrapping"],"backgroundTag":"command-substitution-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}