{"record":{"id":"e53813a9043bf654","repo":"jdx/mise","slug":"brew-cask-command-wrapper-requires-content-or-exe-e53813","errorCode":null,"errorMessage":"brew-cask: command_wrapper requires content or executable, not both","messagePattern":"brew-cask: command_wrapper requires content or executable, not both","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":5156,"sourceCode":"        bail!(\n            \"brew-cask: command_wrapper has unsupported option {}\",\n            unsupported.join(\", \")\n        );\n    }\n    let content = options\n        .get(\"content\")\n        .and_then(Value::as_str)\n        .map(str::to_string);\n    let executable = options\n        .get(\"executable\")\n        .and_then(Value::as_str)\n        .map(str::to_string);\n    match (content.is_some(), executable.is_some()) {\n        (false, false) => {\n            bail!(\"brew-cask: command_wrapper requires content or executable\")\n        }\n        (true, true) => {\n            bail!(\"brew-cask: command_wrapper requires content or executable, not both\")\n        }\n        _ => {}\n    }\n    let args = options\n        .get(\"args\")\n        .map(|args| {\n            args.as_array()\n                .ok_or_else(|| eyre!(\"brew-cask: command_wrapper args must be an array\"))?\n                .iter()\n                .map(|arg| {\n                    arg.as_str()\n                        .map(str::to_string)\n                        .ok_or_else(|| eyre!(\"brew-cask: command_wrapper args must be strings\"))\n                })\n                .collect::<Result<Vec<_>>>()\n        })\n        .transpose()?\n        .unwrap_or_default();","sourceCodeStart":5138,"sourceCodeEnd":5174,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/src/system/packages/brew/cask.rs#L5138-L5174","documentation":"The content/executable pair is mutually exclusive: 'content' is a literal script body while 'executable' generates an exec wrapper, so setting both leaves the intended behavior undefined and mise bails with 'not both'. This is the (true, true) arm of the same match that enforces the required-field case.","triggerScenarios":"Wrapper options containing both keys, e.g. [\"tool\", {\"content\": \"#!/bin/sh ...\", \"executable\": \"bin/tool\"}] — caught at src/system/packages/brew/cask.rs:5158 right after both fields are parsed.","commonSituations":"Copy-paste of an example wrapper followed by adding the other field; metadata merges that union both variants; editing a wrapper from executable to content without deleting the old key.","solutions":["Keep 'executable' if you just want to wrap a payload binary with args/env; delete the 'content' key.","Keep 'content' if you need a fully custom script; delete the 'executable' key.","In editors/linters for cask metadata, enforce the xor of the two keys."],"exampleFix":"# before\ncommand_wrapper: [[\"tool\", {\"content\": \"#!/bin/sh\\nexec tool \\\"$@\\\"\", \"executable\": \"bin/tool\"}]]\n\n# after\ncommand_wrapper: [[\"tool\", {\"executable\": \"bin/tool\"}]]","handlingStrategy":"validation","validationCode":"jq -e '.command_wrapper[]? | .[1] | (has(\"content\") and has(\"executable\")) | not' cask.json >/dev/null || echo 'wrapper sets both content and executable'","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Enforce the content/executable xor in metadata lint and editor snippets.","When converting a wrapper from content to executable, delete the old key in the same commit.","Treat 'both set' as a merge artifact worth investigating, not a feature."],"tags":["brew-cask","command-wrapper","mutually-exclusive","mise"],"backgroundTag":"mutually-exclusive-config-fields","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}