Hmbown/CodeWhale · error · anyhow::Error

Invalid MCP consent history; contents omitted

Error message

Invalid MCP consent history; contents omitted

What it means

persist_decisions writes import-consent decisions into the MCP config file under a process-wide write lock. Before mutating, it re-parses the existing file; if the file exists but is not valid JSON, the write aborts rather than silently replacing the user's malformed consent history with empty state — contents are deliberately not echoed.

Solutions

  1. Open the config file, fix or remove the invalid JSON, and retry the import flow
  2. If the history is unrecoverable, back up the file, delete it, and let the tool recreate it (prior consent decisions are lost)
  3. Restore the file from version control or backup before re-running the consent flow
Defensive patterns

Strategy: try-catch

Validate before calling

const raw = fs.readFileSync(path, "utf8");
try { JSON.parse(raw); } catch (e) { /* repair or restore the file before proceeding */ }

Try / catch

catch the error, then attempt JSON.parse yourself to locate the corruption; repair or restore from backup before retrying the consent flow

Prevention

When it happens

Trigger: `persist_decisions` (via apply_reviewed_import or the consent-transaction test) is called on a path whose config file exists but contains corrupted/partial JSON — a truncated write, manual edit error, or leftover merge-conflict markers.

Common situations: A crashed earlier write left a half-written file; a git merge of the config left `<<<<<<<` markers; the file was saved with a non-JSON encoding or stray BOM.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/2a24d4d31df7649d. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/mcp/external_import.rs:297

fn hex_sha256(bytes: &[u8]) -> String {
    let digest = Sha256::digest(bytes);
    digest.iter().map(|b| format!("{b:02x}")).collect()
}

/// Record decisions against the latest consent document under the shared
/// process lock. Malformed history is never silently replaced with empty state.
pub fn persist_decisions(
    path: &Path,
    candidates: &[ImportCandidate],
    decisions: &HashMap<String, ImportDecision>,
    now_unix: u64,
) -> anyhow::Result<()> {
    super::validate_mcp_config_path(path)?;
    codewhale_config::with_config_write_lock(path, |path| {
        let original = super::read_mcp_config_file(path)?;
        let mut raw: Value = match original.as_deref() {
            Some(raw) => serde_json::from_str(raw)
                .map_err(|_| anyhow::anyhow!("Invalid MCP consent history; contents omitted"))?,
            None => serde_json::json!({}),
        };
        anyhow::ensure!(raw.is_object(), "MCP consent history must be an object");
        let mut store: ImportConsentStore = if original.is_none() {
            ImportConsentStore::default()
        } else {
            serde_json::from_value(raw.clone())
                .map_err(|_| anyhow::anyhow!("Invalid MCP consent history; contents omitted"))?
        };
        let before = serde_json::to_value(&store)?;
        record_decisions(&mut store, candidates, decisions, now_unix);
        let after = serde_json::to_value(&store)?;
        super::apply_json_delta(&mut raw, &before, &after);
        let rendered = serde_json::to_vec_pretty(&raw)?;
        if rendered.len() as u64 > super::MAX_MCP_CONFIG_BYTES {
            anyhow::bail!("MCP consent history exceeds size limit");
        }
        crate::utils::write_atomic(path, &rendered)?;

View on GitHub (pinned to 73e0f67d83)