Hmbown/CodeWhale · critical
reviewed plugin executable bytes changed before spawn
Error message
reviewed plugin executable bytes changed before spawn
What it means
Thrown by the reviewed-plugin launch guard in bind_file (crates/tui/src/mcp.rs:912). Before spawning a trusted plugin's executable, Codewhale re-hashes the file (SHA-256 over the domain tag "codewhale-plugin-file-bytes-v1\0" plus the bytes) and compares it with the hash recorded in the byte inventory captured at /plugin trust time. A mismatch means the exact bytes that were reviewed are no longer on disk, so launch fails closed. This is a deliberate tamper/TOCTOU check, not an I/O malfunction.
Source
Thrown at crates/tui/src/mcp.rs:912
let mut hasher = sha2::Sha256::new();
hasher.update(b"codewhale-plugin-file-bytes-v1\0");
let mut buffer = [0_u8; 64 * 1024];
loop {
let read = file
.read(&mut buffer)
.context("read reviewed launch file")?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
let actual = hasher
.finalize()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
if &actual != expected {
anyhow::bail!("reviewed plugin executable bytes changed before spawn");
}
file.seek(std::io::SeekFrom::Start(0))
.context("rewind reviewed launch file after verification")?;
#[cfg(unix)]
let launch_path = {
use std::os::fd::AsRawFd as _;
let fd = file.as_raw_fd();
// SAFETY: `fd` is owned by `file`; clearing only FD_CLOEXEC keeps
// that same descriptor available across the imminent exec.
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 || unsafe { libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) } < 0
{
anyhow::bail!("failed to inherit reviewed plugin executable descriptor");
}
#[cfg(target_os = "linux")]
let prefix = "/proc/self/fd";
#[cfg(not(target_os = "linux"))]View on GitHub (pinned to 8880682c63)
Solutions
- Re-trust the plugin: run /plugin reload, inspect /plugin show <name>, repeat the displayed /plugin trust <name> <token> command, then /plugin enable <name> (the exact steps the adjacent validate_before_use message prescribes at mcp.rs:719-721).
- If the change was intentional, go through the update path (/plugin update) so the byte inventory is regenerated instead of bypassed.
- Find what rewrote the file: compare mtimes (stat on the bundle files), disable sync clients (Dropbox/OneDrive) over the plugins directory, stop build watchers that rewrite outputs.
- If nothing you did explains the change, treat it as tampering: verify the file's provenance out-of-band before re-trusting.
Example fix
// before: plugin binary rebuilt after trust -> spawn aborts: // reviewed plugin executable bytes changed before spawn // after: refresh the trust receipt after every intentional byte change // /plugin reload // /plugin show my-plugin // /plugin trust my-plugin <token-shown-by-show> // /plugin enable my-plugin
Defensive patterns
Strategy: validation
Validate before calling
use sha2::{Digest, Sha256};
use std::{fs::File, io::Read, path::Path};
/// Mirrors the launcher's check: SHA-256 over the domain tag + file bytes.
fn launch_bytes_match(path: &Path, expected_hex: &str) -> anyhow::Result<bool> {
let mut file = File::open(path)?;
let mut hasher = Sha256::new();
hasher.update(b"codewhale-plugin-file-bytes-v1\0");
let mut buf = [0u8; 64 * 1024];
loop {
let n = file.read(&mut buf)?;
if n == 0 { break; }
hasher.update(&buf[..n]);
}
let actual: String = hasher.finalize().iter().map(|b| format!("{b:02x}")).collect();
Ok(actual == expected_hex)
} Try / catch
match McpConnection::connect_with_policy(name, config, &timeouts, policy).await {
Ok(conn) => conn,
Err(err) if err.to_string().contains("reviewed plugin executable bytes changed") => {
// Fail-closed by design: surface the re-trust steps, never retry unchanged.
Err(err.context("plugin bytes drifted from review; re-run /plugin trust"))?
}
Err(err) => return Err(err),
} Prevention
- Re-run /plugin trust after every intentional change to plugin bytes.
- Keep reviewed plugin bundles in a directory no build step or sync client writes to.
- Treat any unexplained occurrence as tampering and investigate before re-trusting.
When it happens
Trigger: Spawning an MCP server from a plugin whose executable or bound bundle file was modified after /plugin trust recorded its hash: rebuilding the plugin, a package manager / sync client rewriting the file, an editor touching the script, or /plugin update landing without re-trusting.
Common situations: Plugin authors iterating on a bundle and re-running the TUI without re-trusting; build watchers or CI rewriting plugin outputs between trust and use; marketplace updates arriving mid-session; the rare case of actual tampering, which this guard exists to catch.
Related errors
- reviewed plugin stage could not be opened for launch
- reviewed plugin stage changed before stdio launch
- reviewed plugin stdio cwd escaped its staged root
- Refusing to {operation} MCP server '{server_name}' from plug
- Refusing MCP server '{server_name}': its remote endpoint no
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/a6d02ead62d73cd0.
Report an issue: GitHub.