{"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":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/shell.rs","lineNumber":4213,"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":4195,"sourceCodeEnd":4231,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/tools/shell.rs#L4195-L4231","documentation":"This error is thrown by `hardened_readonly_argv` in the read-only shell hardening path. When a classifier-approved read command starts with `git`, the hardener walks past the known preamble flags `--no-pager` and `-C <dir>` to locate the Git subcommand, then splices safety flags after it. If the argv ends before a subcommand is found (only preamble flags were supplied), the hardener refuses to proceed rather than guess what Git should run.","triggerScenarios":"Calling the read-only shell path with a command like `git --no-pager` or `git -C /some/dir` where tokenizing stops after the preamble flags and no subcommand (diff/log/show/status/ls-files/blame/grep) follows. Also produced if shell_words parsing yields fewer tokens than expected after path normalization.","commonSituations":"Programmatically assembled Git commands that end after global flags; a command string truncated by a quoting bug so `git -C ..` loses its subcommand; classifier policy updated to admit new preamble flags without extending the skip loop, so the loop consumes everything and leaves nothing at subcommand_index.","solutions":["Include a supported read-only subcommand after any global flags, e.g. `git --no-pager diff` instead of `git --no-pager`","Check the command string for quoting/truncation so all tokens survive shell_words::split","If a new global Git flag must be supported, extend the preamble-skip match in hardened_readonly_argv so the subcommand index lands on the real subcommand"],"exampleFix":"// before\nrun_readonly(\"git --no-pager\")\n// after\nrun_readonly(\"git --no-pager diff\")","handlingStrategy":"validation","validationCode":"fn has_git_subcommand(cmd: &str) -> bool {\n    let argv = shell_words::split(cmd).unwrap_or_default();\n    if argv.first().map(String::as_str) != Some(\"git\") { return true; }\n    let mut i = 1;\n    while matches!(argv.get(i).map(String::as_str), Some(\"--no-pager\") | Some(\"-C\")) {\n        i += if argv.get(i).map(String::as_str) == Some(\"-C\") { 2 } else { 1 };\n    }\n    argv.get(i).is_some()\n}\nif !has_git_subcommand(&cmd) { eprintln!(\"git command needs a subcommand\"); }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always pair global Git flags (--no-pager, -C) with an explicit subcommand","Validate assembled command strings have a subcommand token before passing to the read-only shell","Beware shell quoting that can silently drop trailing tokens"],"tags":["git","shell","security","argument-parsing"],"backgroundTag":"missing-required-argument","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}