{"record":{"id":"2e9502c36bfd8ace","repo":"Hmbown/CodeWhale","slug":"classifier-approved-git-read-did-not-keep-its-subc","errorCode":null,"errorMessage":"classifier-approved Git read did not keep its subcommand in argv[1]","messagePattern":"classifier-approved Git read did not keep its subcommand in argv\\[1\\]","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/shell.rs","lineNumber":3768,"sourceCode":"                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                    ],\n                );\n            }\n            \"status\" | \"ls-files\" | \"blame\" | \"grep\" => {}\n            _ => {\n                return Err(anyhow!(\n                    \"classifier-approved Git read did not keep its subcommand in argv[1]\"\n                ));\n            }\n        }\n    }\n\n    let program = argv.remove(0);\n    Ok((program, argv))\n}\n\nfn enforce_readonly_workspace_operands(\n    command: &str,\n    workspace: &std::path::Path,\n    effective_cwd: &std::path::Path,\n) -> Result<(), ToolError> {\n    let argv = shell_words::split(command).map_err(|error| {\n        ToolError::invalid_input(format!(\n            \"Could not parse read-only command arguments: {error}\"","sourceCodeStart":3750,"sourceCodeEnd":3786,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/tools/shell.rs#L3750-L3786","documentation":"After skipping the admitted preamble, the hardener only knows how to harden git's read subcommands: `diff`, `log`, `show` (get flag splices) plus `status`, `ls-files`, `blame`, `grep` (pass through bare). Any other subcommand reaches this refusal — the classifier admitted something the hardener cannot prove safe, so the path fails closed. The message text says 'argv[1]' but the check runs at the computed subcommand position after `-C`/`--no-pager`.","triggerScenarios":"A classifier-admitted git read whose subcommand falls outside {diff, log, show, status, ls-files, blame, grep} — e.g. `git -C dir remote -v`, `git describe --tags`, `git shortlog` — reaching the match's `_` arm at shell.rs:3768.","commonSituations":"Classifier allowlist drift admitting new git subcommands; agents probing less-common read-only git verbs that look harmless.","solutions":["Restrict agent git reads to the hardened set: diff/log/show/status/ls-files/blame/grep","Route other git reads through a normal (approved, non-readonly) shell call or the File tools","Report the admitted-but-unhardened subcommand as a classifier/hardener mismatch bug","For `remote -v` style info, prefer `gh` reads or configuration inspection via allowed tools"],"exampleFix":"# before: subcommand outside the hardened set\ngit -C repo remote -v\n# after: hardened read, or an approved full shell call\ngit -C repo status   # or dispatch 'git remote -v' through the non-readonly path","handlingStrategy":"validation","validationCode":"const HARDENED_GIT_READS: &[&str] = &[\"diff\", \"log\", \"show\", \"status\", \"ls-files\", \"blame\", \"grep\"];\n\nfn git_subcommand_if_any(argv: &[String]) -> Option<&str> {\n    if argv.first().map(String::as_str) != Some(\"git\") {\n        return None;\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).map(String::as_str)\n}\n\nif let Some(sub) = git_subcommand_if_any(&argv)\n    && !HARDENED_GIT_READS.contains(&sub)\n{\n    return report(format!(\"git {sub} is outside the hardened read set; use an approved full shell call\"));\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(\"did not keep its subcommand\") => {\n        report(\"switch to a hardened git read (diff/log/show/status/ls-files/blame/grep) or use an approved full shell call\")\n    }\n    Err(err) => Err(err),\n}","preventionTips":["Restrict agent git reads to diff/log/show/status/ls-files/blame/grep","Teach command generators the hardened subcommand set","Route novel git subcommands through the approval-gated non-readonly path","Report classifier-admitted subcommands that reach this refusal as drift"],"tags":["readonly-shell","git","allowlist","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"}