{"record":{"id":"6c9a942505b8358a","repo":"Hmbown/CodeWhale","slug":"permission-rule-index-changed-before-removal","errorCode":null,"errorMessage":"permission rule index changed before removal","messagePattern":"permission rule index changed before removal","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/config/src/lib.rs","lineNumber":6355,"sourceCode":"        }\n        toml_edit::Item::Value(value) => {\n            let Some(rules) = value.as_array_mut() else {\n                bail!(\"`rules` in permissions.toml must be an array\");\n            };\n            rules.push(toml_edit::Value::InlineTable(permission_rule_inline_table(\n                rule,\n            )));\n            Ok(())\n        }\n        _ => bail!(\"`rules` in permissions.toml must be an array\"),\n    }\n}\n\nfn remove_permission_rule_item(item: &mut toml_edit::Item, index: usize) -> Result<Option<String>> {\n    match item {\n        toml_edit::Item::ArrayOfTables(rules) => {\n            if index >= rules.len() {\n                bail!(\"permission rule index changed before removal\");\n            }\n            let file_header = if index == 0 {\n                rules\n                    .get(index)\n                    .and_then(|rule| rule.decor().prefix())\n                    .and_then(toml_edit::RawString::as_str)\n                    .map(str::to_owned)\n            } else {\n                None\n            };\n            rules.remove(index);\n            if let Some(header) = file_header.as_deref()\n                && let Some(next_rule) = rules.get_mut(0)\n            {\n                let next_prefix = next_rule\n                    .decor()\n                    .prefix()\n                    .and_then(toml_edit::RawString::as_str)","sourceCodeStart":6337,"sourceCodeEnd":6373,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/config/src/lib.rs#L6337-L6373","documentation":"remove_permission_rule_item checks that the requested index still exists in the ArrayOfTables just before splicing it out. Even though the removal token was verified moments earlier under the lock, this guard defends against the index having drifted relative to the edited document item.","triggerScenarios":"remove_permission_rule with an index >= number of [[rules]] tables in the document item, typically from a stale snapshot whose index no longer exists because the file shrank or was restructured between listing and removal.","commonSituations":"Removing the last rule after another process already removed it; the user hand-deleted rules while the removal was pending; index arithmetic (1-based vs 0-based) confusion leading to an out-of-range index.","solutions":["Reload with load_permissions_snapshot and retry with a fresh index and token","Remember the API is 0-based: pass index-1 for what displays as rule N in listings that number from 1","Check the current rule count (snap.permissions.rules.len()) before calling remove"],"exampleFix":"// before\nremove_permission_rule(None, 3, &token)?; // only 3 rules (0..=2)\n\n// after\nlet snap = load_permissions_snapshot(None)?;\nif 3 < snap.permissions.rules.len() {\n    remove_permission_rule(None, 3, &snap.removal_tokens[3])?;\n}","handlingStrategy":"validation","validationCode":"let snap = load_permissions_snapshot(config_path)?;\nif index >= snap.permissions.rules.len() {\n    return Err(anyhow::anyhow!(\"index {} out of range; {} rules\", index, snap.permissions.rules.len()));\n}","typeGuard":"fn rule_index_in_range(snap: &PermissionsSnapshot, index: usize) -> bool {\n    index < snap.permissions.rules.len()\n}","tryCatchPattern":"match remove_permission_rule(path, index, &token) {\n    Err(e) if e.to_string().contains(\"index changed before removal\") => {\n        let snap = load_permissions_snapshot(path)?;\n        if index < snap.removal_tokens.len() {\n            remove_permission_rule(path, index, &snap.removal_tokens[index])?\n        }\n    }\n    other => other?,\n}","preventionTips":["Re-snapshot before every removal; never reuse indices from an old listing","Treat rule indices as 0-based; display listings that count from 1 need index-1","Serialize permission mutations; do not let multiple processes edit the file concurrently"],"tags":["config","permissions","index-out-of-range"],"backgroundTag":"index-out-of-range","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}