{"record":{"id":"d488e184c9cc6e5a","repo":"astrid-runtime/astrid","slug":"mcp-attach-registration-hook-token-is-invalid","errorCode":null,"errorMessage":"MCP attach registration hook_token is invalid","messagePattern":"MCP attach registration hook_token is invalid","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/mcp/gateway.rs","lineNumber":953,"sourceCode":"    }\n    validate_workspace(&registration.workspace_abs)?;\n    Ok(())\n}\n\nfn authenticate_registration(\n    registration: &AttachRegistration,\n    state: &GatewayState,\n) -> Result<astrid_core::PrincipalId> {\n    let principal = super::lifecycle::resolve_principal(Some(&registration.principal))?;\n    if principal != state.principal {\n        anyhow::bail!(\n            \"MCP attach registration principal '{}' is not the authenticated gateway principal '{}'\",\n            principal,\n            state.principal\n        );\n    }\n    if registration.hook_token != state.hook_token {\n        anyhow::bail!(\"MCP attach registration hook_token is invalid\");\n    }\n    Ok(principal)\n}\n\nfn mint_hook_token() -> String {\n    format!(\n        \"{}{}\",\n        Uuid::new_v4().as_simple(),\n        Uuid::new_v4().as_simple()\n    )\n}\n\nfn mint_boot_token() -> String {\n    Uuid::new_v4().as_simple().to_string()\n}\n\nfn validate_workspace(value: &str) -> Result<PathBuf> {\n    if value.is_empty() {","sourceCodeStart":935,"sourceCodeEnd":971,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/mcp/gateway.rs#L935-L971","documentation":"authenticate_registration compares the hook_token in the attach registration against the token stored in GatewayState. A mismatch means the caller does not share the gateway's secret, so the request is rejected even if the principal matches. This complements the principal check as a bearer-token authentication layer.","triggerScenarios":"serve_attach (or tests forged_principal_cannot_select_another_gateway_uplink / missing_hook_token_is_rejected_before_uplink_selection) supplies a hook_token that differs from state.hook_token — e.g. a token from a previous gateway run, another gateway, or a fabricated value.","commonSituations":"Stale readiness metadata from a previous gateway generation read by the client; two gateways running concurrently and the client mixed up tokens; token rotated on the gateway side but the client cached the old one.","solutions":["Re-read the current gateway readiness record and use its hook_token verbatim (no trimming/transforming)","Confirm you're connecting to the correct gateway socket for your session","Restart the gateway and use the freshly minted token","Check the token wasn't truncated or re-encoded during serialization"],"exampleFix":"// before\nhook_token: old_cached_token,\n// after\nlet ready = read_gateway_ready()?.expect(\"gateway ready record\");\nregistration.hook_token = ready.hook_token;","handlingStrategy":"validation","validationCode":"if reg.hook_token != state.hook_token { return Err(\"stale hook_token — re-read readiness record\"); }","typeGuard":null,"tryCatchPattern":"if let Err(e) = authenticate_registration(&reg, &state) {\n    if e.to_string().contains(\"hook_token is invalid\") { refresh_token_and_retry(); }\n}","preventionTips":["Re-read hook_token from the readiness file on every attach, don't cache across restarts","Compare tokens byte-for-byte without trimming/casing","Detect gateway restarts (pid change) and refresh credentials"],"tags":["authentication","security","mcp"],"backgroundTag":"authentication-required","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}