{"record":{"id":"7a4758649a4e2db7","repo":"jdx/mise","slug":"cli-spec-entry-names-bin-in-format-format","errorCode":null,"errorMessage":"cli-spec entry names {bin:?} in format {format:?}","messagePattern":"cli-spec entry names (.+?) in format (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/packslip.rs","lineNumber":1539,"sourceCode":"        let attempt = match source {\n            CompletionSource::File(path) => file::read_to_string(&path),\n            CompletionSource::Spec { format, bin, path } => {\n                derive_from_spec(&format, &bin, &path, shell)\n            }\n            CompletionSource::Exec(argv, env) => run_tool(config, &backend, &tv, &argv, &env).await,\n            CompletionSource::SpecExec {\n                format,\n                bin,\n                argv,\n                env,\n            } => {\n                // Any failure here is one more reason to try the next source,\n                // not the end of the search. The spec is kept in the install:\n                // a script derived from it names the file at completion time,\n                // so it has to outlive this command.\n                async {\n                    if !file::is_plain_file_name(&bin) || !file::is_plain_file_name(&format) {\n                        bail!(\"cli-spec entry names {bin:?} in format {format:?}\");\n                    }\n                    let spec = run_tool(config, &backend, &tv, &argv, &env).await?;\n                    // A spec generated for one shell, as `{shell}` in the\n                    // command allows, is not the spec for another, and two\n                    // shells generating at once must not read each other's\n                    // half-written file. `shell` is a plain file name: the\n                    // cache path above refuses anything else.\n                    let dir = install_path.join(RESOURCES_DIR).join(\"specs\").join(shell);\n                    file::create_dir_all(&dir)?;\n                    let path = dir.join(format!(\"{bin}.{format}\"));\n                    file::write_atomic(&path, &spec)?;\n                    derive_from_spec(&format, &bin, &path, shell)\n                }\n                .await\n            }\n        };\n        match attempt {\n            Ok(script) if script.trim().is_empty() => {","sourceCodeStart":1521,"sourceCodeEnd":1557,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/packslip.rs#L1521-L1557","documentation":"When completion_script iterates cli-spec sources, each entry's generated binary name and format (e.g. `fish`, `bash`) must both be plain file names because they are used to build cache/resource paths. If a cli-spec entry yields a bin or format containing path components or traversal characters, mise throws this before running the tool to generate the spec.","triggerScenarios":"A packslip cli-spec entry whose completion command produces a bin name or format with `/`, `.`, `..` (e.g. the spec command template or `--format` placeholder interpolation yields `sub/dir` or an absolute path).","commonSituations":"Malformed cli-spec in the packslip manifest (command template inserting directories); upstream changed its completion command output naming; a spec command flag value containing slashes interpolated into bin/format.","solutions":["Fix the cli-spec entry in the tool's packslip manifest so the bin and format placeholders resolve to bare names (`bash`, `fish`).","Check the tool's completion command for arguments emitting path-like values and strip them to file names in the spec template.","Report/upgrade the packslip upstream if the release manifest is wrong.","As a workaround, generate the completion manually with the tool's own completion command."],"exampleFix":"// before\n[resources.cli-spec]\ncommand = \"mytool completions --out dir/{format}\"\n// after\n[resources.cli-spec]\ncommand = \"mytool completions --format {format}\"","handlingStrategy":"validation","validationCode":"fn is_safe_component(s: &str) -> bool { !s.is_empty() && s != \".\" && s != \"..\" && !s.contains('/') && !s.contains('\\\\') }\nassert!(is_safe_component(bin) && is_safe_component(format));","typeGuard":"fn is_plain_file_name(s: &str) -> bool { !s.is_empty() && !s.contains('/') && !s.contains('\\\\') && s != \".\" && s != \"..\" }","tryCatchPattern":null,"preventionTips":["Keep cli-spec command templates free of directory components in bin/format placeholders","Test packslip manifests across shells before publishing releases","Strip paths from tool completion output before templating into cli-spec"],"tags":["packslip","completion","path-validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}