{"record":{"id":"18fd4457fd300723","repo":"Hmbown/CodeWhale","slug":"reviewed-plugin-stage-changed-before-stdio-launch","errorCode":null,"errorMessage":"reviewed plugin stage changed before stdio launch","messagePattern":"reviewed plugin stage changed before stdio launch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/tui/src/mcp.rs","lineNumber":667,"sourceCode":"        server_name: &str,\n        command: &str,\n        args: &[String],\n        cwd: Option<&Path>,\n    ) -> Result<ReviewedStdioLaunch> {\n        self.validate_before_stdio_spawn(server_name)?;\n        let staged_root = self\n            .authority\n            .staged_manifest\n            .parent()\n            .context(\"reviewed plugin stage manifest has no parent\")?;\n        let validated = crate::plugins::manifest::PluginManifest::validate_from_path(\n            &self.authority.staged_manifest,\n        )\n        .map_err(|_| anyhow::anyhow!(\"reviewed plugin stage could not be opened for launch\"))?;\n        if validated.content_hash != self.authority.content_hash\n            || validated.capability_hash != self.authority.capability_hash\n        {\n            anyhow::bail!(\"reviewed plugin stage changed before stdio launch\");\n        }\n\n        let mut launch = ReviewedStdioLaunch {\n            command: std::ffi::OsString::from(command),\n            args: args.iter().map(std::ffi::OsString::from).collect(),\n            cwd: cwd.map(Path::to_path_buf),\n            opened_files: Vec::new(),\n            #[cfg(unix)]\n            cwd_fd: None,\n        };\n        if Path::new(command).is_absolute() {\n            launch.bind_command(staged_root, Path::new(command), &validated.file_hashes)?;\n        }\n        for (index, argument) in args.iter().enumerate() {\n            let path = Path::new(argument);\n            if path.is_absolute() && path.starts_with(staged_root) && path.is_file() {\n                launch.args[index] = launch.bind_file(staged_root, path, &validated.file_hashes)?;\n            }","sourceCodeStart":649,"sourceCodeEnd":685,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/crates/tui/src/mcp.rs#L649-L685","documentation":"TOCTOU guard at stdio launch: the staged manifest is re-validated and its content_hash and capability_hash are compared against the hashes recorded when the plugin was reviewed; any mismatch aborts the launch. It fires when plugin files changed on disk after review - tampering, but also legitimate edits or version bumps that bypassed the review flow. The reviewed approval never covers the changed bytes.","triggerScenarios":"Editing files inside the plugin stage or reviewed source between approval and launch, an update overwriting the bundle without redoing trust, external processes (sync tools, CI deploys) touching staged files, or deliberate tampering.","commonSituations":"Developers editing plugin sources in place, CI redeploying bundles under running sessions, dropbox-style sync touching staged files, supply-chain style attempts to swap code after review.","solutions":["If the change is intentional: /plugin reload, inspect /plugin show <name>, repeat the displayed trust command, then /plugin enable <name> so the new hashes are reviewed","Stop editing staged or bundled files in place; publish a new bundle version instead","If nothing was changed by you, audit what modified the stage (mtimes, audit logs) before re-trusting - treat it as possible tampering"],"exampleFix":"# error: reviewed plugin stage changed before stdio launch\n/plugin reload\n/plugin show my-plugin   # confirm the new hashes are expected\n# repeat the displayed trust command, then:\n/plugin enable my-plugin","handlingStrategy":"retry","validationCode":"// Detect drift before launch: re-hash the stage and compare with the review record\nfn stage_matches(review: &ReviewRecord, staged: &std::path::Path) -> bool {\n    PluginManifest::validate_from_path(staged)\n        .map(|v| v.content_hash == review.content_hash && v.capability_hash == review.capability_hash)\n        .unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"// Never bypass: restage + re-review is the only sanctioned recovery\nif err.to_string().contains(\"changed before stdio launch\") {\n    reload_and_retrust(&plugin)?; // /plugin reload + trust + /plugin enable\n    return retry_launch_once();\n}","preventionTips":["Never edit plugin bundles under the stage root; version and republish instead","Re-run review after any plugin update, however small","Investigate unexpected hash mismatches before re-trusting; they can indicate tampering","Keep reviewed bundles on immutable (read-only) storage between reviews"],"tags":["plugin","security","tampering","hash","toctou"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}