{"record":{"id":"f20ce19f1b3cc843","repo":"Hmbown/CodeWhale","slug":"classifier-approved-git-read-was-missing-its-liter","errorCode":null,"errorMessage":"classifier-approved Git read was missing its literal subcommand","messagePattern":"classifier-approved Git read was missing its literal subcommand","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/shell.rs","lineNumber":3745,"sourceCode":"    if argv.first().is_some_and(|program| program == \"git\") {\n        // The agent read-only classifier admits `git -C <dir>` and\n        // `git --no-pager` before the subcommand; keep the preamble but\n        // locate the subcommand after it so the hardening flags splice in\n        // the right place. `-C` targets were already workspace-checked by\n        // `enforce_readonly_workspace_operands`.\n        let mut subcommand_index = 1;\n        while let Some(flag) = argv.get(subcommand_index) {\n            match flag.as_str() {\n                \"--no-pager\" => subcommand_index += 1,\n                \"-C\" => subcommand_index += 2,\n                _ => break,\n            }\n        }\n        let subcommand = argv\n            .get(subcommand_index)\n            .map(String::as_str)\n            .ok_or_else(|| {\n                anyhow!(\"classifier-approved Git read was missing its literal subcommand\")\n            })?;\n        match subcommand {\n            \"diff\" => {\n                let at = subcommand_index + 1;\n                argv.splice(\n                    at..at,\n                    [\"--no-ext-diff\".to_string(), \"--no-textconv\".to_string()],\n                );\n            }\n            \"log\" | \"show\" => {\n                let at = subcommand_index + 1;\n                argv.splice(\n                    at..at,\n                    [\n                        \"--no-ext-diff\".to_string(),\n                        \"--no-textconv\".to_string(),\n                        \"--no-show-signature\".to_string(),\n                    ],","sourceCodeStart":3727,"sourceCodeEnd":3763,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/tools/shell.rs#L3727-L3763","documentation":"For `git` commands the hardener skips the admitted preamble flags (`--no-pager`, `-C <dir>`) and then requires a literal subcommand token at that position so it can splice subcommand-specific hardening flags (`--no-ext-diff`, `--no-textconv`, `--no-show-signature`). This error fires when the tokens end right after the preamble — no subcommand exists to harden — and the path fails closed.","triggerScenarios":"A classifier-admitted git invocation whose token list is only preamble: `git -C repo` (the `-C` consumed `repo` as its value, leaving nothing) or `git --no-pager` with nothing following; typically a concatenation bug where the subcommand variable was empty.","commonSituations":"Commands assembled by string interpolation with an empty subcommand; a `-C` argument that accidentally swallowed the only remaining token.","solutions":["Always emit a concrete git subcommand after any `-C <dir>` / `--no-pager` preamble (e.g. `git -C repo status`)","Validate that commands starting with `git` have at least two non-preamble tokens before dispatch","Route partial git invocations through a full-permission shell call with approval instead"],"exampleFix":"# before: preamble only, subcommand missing\ngit -C repo\n# after: preamble plus a hardened read subcommand\ngit -C repo status","handlingStrategy":"validation","validationCode":"fn git_has_literal_subcommand(command: &str) -> bool {\n    let Ok(argv) = shell_words::split(command) else { return false };\n    if argv.first().map(String::as_str) != Some(\"git\") {\n        return true;\n    }\n    let mut i = 1;\n    while let Some(flag) = argv.get(i) {\n        match flag.as_str() {\n            \"--no-pager\" => i += 1,\n            \"-C\" => i += 2,\n            _ => break,\n        }\n    }\n    argv.get(i).is_some()\n}","typeGuard":"fn hardenable_git_read(command: &str) -> bool {\n    let Ok(argv) = shell_words::split(command) else { return false };\n    if argv.first().map(String::as_str) != Some(\"git\") { return true; }\n    let mut i = 1;\n    while let Some(flag) = argv.get(i) {\n        match flag.as_str() {\n            \"--no-pager\" => i += 1,\n            \"-C\" => i += 2,\n            _ => break,\n        }\n    }\n    matches!(\n        argv.get(i).map(String::as_str),\n        Some(\"diff\" | \"log\" | \"show\" | \"status\" | \"ls-files\" | \"blame\" | \"grep\")\n    )\n}","tryCatchPattern":"match hardened_readonly_argv(command) {\n    Ok(parsed) => Ok(parsed),\n    Err(err) if err.to_string().contains(\"missing its literal subcommand\") => {\n        report(\"emit a concrete git subcommand after -C/--no-pager and retry\")\n    }\n    Err(err) => Err(err),\n}","preventionTips":["Always emit a subcommand after `git -C <dir>` — the flag consumes the next token","Validate that git commands have a token beyond the preamble before dispatch","Assemble git commands from non-empty parts; check interpolated subcommand variables","Route partial invocations through an approved full shell call"],"tags":["readonly-shell","git","argv-validation","security","rust"],"backgroundTag":"git-subcommand-not-allowlisted","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}