{"record":{"id":"44b1fe2fbc488567","repo":"astrid-runtime/astrid","slug":"workspace-capsule-directory-changed-during-manifes","errorCode":null,"errorMessage":"Workspace capsule directory changed during manifest lookup: {e}","messagePattern":"Workspace capsule directory changed during manifest lookup: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-cli/src/commands/secret.rs","lineNumber":133,"sourceCode":"        .join(capsule.as_str())\n        .join(\"Capsule.toml\");\n    if principal_manifest.exists() {\n        return read_capsule_manifest(&principal_manifest).map(Some);\n    }\n\n    let Some(workspace_root) = workspace_root else {\n        return Ok(None);\n    };\n    let workspace = workspace_layout\n        .resolve(workspace_root)\n        .context(\"Failed to resolve the selected workspace\")?;\n    let capsules_dir = workspace\n        .verify_tree(\"capsules\")\n        .context(\"Workspace capsule directory is unsafe\")?;\n    let workspace_manifest = capsules_dir.join(capsule.as_str()).join(\"Capsule.toml\");\n    if !workspace_manifest.exists() {\n        workspace.verify_tree(\"capsules\").map_err(|e| {\n            anyhow::anyhow!(\"Workspace capsule directory changed during manifest lookup: {e}\")\n        })?;\n        return Ok(None);\n    }\n    let manifest = read_capsule_manifest(&workspace_manifest)?;\n    workspace\n        .verify_tree(\"capsules\")\n        .context(\"Workspace capsule directory changed while reading its manifest\")?;\n    Ok(Some(manifest))\n}\n\n#[cfg(test)]\nfn read_capsule_manifest(manifest_path: &Path) -> Result<CapsuleManifest> {\n    let contents = fs::read_to_string(manifest_path)\n        .with_context(|| format!(\"Failed to read {}\", manifest_path.display()))?;\n    let manifest: CapsuleManifest = toml::from_str(&contents)\n        .with_context(|| format!(\"Failed to parse {}\", manifest_path.display()))?;\n    Ok(manifest)\n}","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-cli/src/commands/secret.rs#L115-L151","documentation":"Secret capsule manifests are resolved by verifying the workspace `capsules` tree multiple times (TOCTOU protection). When a capsule manifest does not exist in the workspace, the code re-verifies the tree before returning `Ok(None)`; if that second verification fails, the capsule directory changed (or was found unsafe) during the lookup and this error is thrown instead of silently returning \"not found\".","triggerScenarios":"Calling secret/capsule manifest lookup (e.g. `load_capsule_manifest_from_home_in_workspace` via `load_capsule_manifest_*`) while the workspace `capsules` directory is modified concurrently — files added/removed, symlink redirects introduced, or permission/structure changes that make `verify_tree(\"capsules\")` fail on the second pass.","commonSituations":"Another process (editor, sync tool, build script) writing to the capsules directory during a secret lookup; a symlink or redirected workspace capsule being swapped in mid-lookup; an attacker-manipulated or corrupted capsules tree; tests that mutate the workspace concurrently.","solutions":["Retry the lookup once the workspace is quiescent; the change may be benign concurrent activity.","Inspect the `capsules` directory for unexpected changes (symlinks, new files, permission changes) that violate the tree verification.","Stop concurrent writers (sync clients, build scripts) or exclude the workspace from live syncing during secret operations.","Treat repeated occurrences as a security signal — verify_tree failing twice suggests an unsafe or redirected capsule tree; restore it from a trusted state."],"exampleFix":"// before\nlet manifest = load_capsule_manifest(&capsule)?; // may fail on concurrent change\n// after\nlet manifest = match load_capsule_manifest(&capsule) {\n    Ok(m) => m,\n    Err(e) if e.to_string().contains(\"changed during manifest lookup\") => {\n        eprintln!(\"workspace changed; retrying\");\n        load_capsule_manifest(&capsule)?\n    }\n    Err(e) => return Err(e),\n};","handlingStrategy":"retry","validationCode":"// Pre-check capsule tree stability before lookup\nlet before = snapshot_capsules_dir(&workspace)?;\nlet manifest = load_capsule_manifest(&capsule)?;\nlet after = snapshot_capsules_dir(&workspace)?;\nassert_eq!(before, after, \"capsules tree changed during lookup\");","typeGuard":"fn capsules_tree_stable(workspace: &Workspace) -> bool {\n    workspace.verify_tree(\"capsules\").is_ok()\n}","tryCatchPattern":"match load_capsule_manifest(&capsule) {\n    Ok(m) => m,\n    Err(e) if e.to_string().contains(\"changed during manifest lookup\") => {\n        eprintln!(\"workspace mutated concurrently; retrying\");\n        load_capsule_manifest(&capsule)?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Pause file sync tools and build scripts during secret lookups","Do not add symlinks or redirects into the capsules directory","Retry once on transient TOCTOU failures","Treat repeated failures as a possible tampering signal and audit the tree"],"tags":["secrets","workspace","toctou","race-condition"],"backgroundTag":"internal-invariant-violation","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}