{"record":{"id":"4abc944ce6a14e8d","repo":"zeroclaw-labs/zeroclaw","slug":"invalid-otp-code-estop-resume-denied","errorCode":null,"errorMessage":"Invalid OTP code; estop resume denied","messagePattern":"Invalid OTP code; estop resume denied","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/security/estop.rs","lineNumber":206,"sourceCode":"\n    fn ensure_resume_is_authorized(\n        &self,\n        otp_code: Option<&str>,\n        otp_validator: Option<&OtpValidator>,\n    ) -> Result<()> {\n        if !self.config.require_otp_to_resume {\n            return Ok(());\n        }\n\n        let code = otp_code\n            .map(str::trim)\n            .filter(|value| !value.is_empty())\n            .context(\"OTP code is required to resume estop state\")?;\n        let validator = otp_validator\n            .context(\"OTP validator is required to resume estop state with OTP enabled\")?;\n        let valid = validator.validate(code)?;\n        if !valid {\n            anyhow::bail!(\"Invalid OTP code; estop resume denied\");\n        }\n        Ok(())\n    }\n\n    fn persist_state(&mut self) -> Result<()> {\n        if let Some(parent) = self.state_path.parent() {\n            fs::create_dir_all(parent).with_context(|| {\n                format!(\n                    \"Failed to create estop state dir {}\",\n                    parent.display().to_string()\n                )\n            })?;\n        }\n\n        let body =\n            serde_json::to_string_pretty(&self.state).context(\"Failed to serialize estop state\")?;\n\n        let temp_path = self","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/security/estop.rs#L188-L224","documentation":"Resuming from estop was refused because the supplied OTP code failed validation (validator.validate returned false). This fires only when the estop config sets require_otp_to_resume=true: the emergency-stop state is security-relevant, so unpausing it demands a current, valid one-time code.","triggerScenarios":"Calling resume() with an expired TOTP window, a code already used (replay protection), a typo, or a code generated from a different secret than the validator's; clock drift between the generator and validator beyond the allowed step.","commonSituations":"Operator's authenticator app out of sync (phone clock skew); validator provisioned with a different base32 secret than the user's; reusing a code that worked for a previous action; NTP broken on the host running the daemon.","solutions":["Generate a fresh code from the current time window and retry — never reuse a code.","Check clock sync on both the authenticator device and the daemon host (NTP).","Verify the validator and the user's authenticator share the same OTP secret/step.","If the secret is lost, re-provision OTP, or temporarily disable require_otp_to_resume with appropriate authorization."],"exampleFix":"// before\nestop.resume(selector, Some(\"123455\"), Some(&validator))?; // typo, denied\n\n// after — sanity-check, then retry with the current code\nlet code = read_current_code().trim().to_string();\nif !(6..=8).contains(&code.len()) || !code.chars().all(|c| c.is_ascii_digit()) {\n    anyhow::bail!(\"otp code malformed before submit\");\n}\nmatch estop.resume(selector, Some(&code), Some(&validator)) {\n    Err(e) if e.to_string().contains(\"Invalid OTP\") => retry_with_next_code(),\n    other => other?,\n}","handlingStrategy":"retry","validationCode":"fn plausible_otp(code: &str) -> bool {\n    let c = code.trim();\n    (6..=8).contains(&c.len()) && c.chars().all(|d| d.is_ascii_digit())\n}","typeGuard":null,"tryCatchPattern":"match estop.resume(selector, Some(&code), Some(&validator)) {\n    Err(e) if e.to_string().contains(\"Invalid OTP\") => {\n        // prompt for the CURRENT code and retry once; do not loop without backoff\n    }\n    other => other?,\n}","preventionTips":["Keep authenticator device and daemon host NTP-synced to avoid window drift.","Provision the OTP validator and the user's authenticator from the same secret.","Pre-validate format (digits, expected length) before consuming a retry attempt.","Never cache or reuse a submitted code."],"tags":["otp","mfa","estop","security","authentication"],"backgroundTag":"mfa-code-invalid","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}