Hmbown/CodeWhale · error
permission rule index changed before removal
Error message
permission rule index changed before removal
What it means
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.
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
Example fix
// before
remove_permission_rule(None, 3, &token)?; // only 3 rules (0..=2)
// after
let snap = load_permissions_snapshot(None)?;
if 3 < snap.permissions.rules.len() {
remove_permission_rule(None, 3, &snap.removal_tokens[3])?;
} Defensive patterns
Strategy: validation
Validate before calling
let snap = load_permissions_snapshot(config_path)?;
if index >= snap.permissions.rules.len() {
return Err(anyhow::anyhow!("index {} out of range; {} rules", index, snap.permissions.rules.len()));
} Type guard
fn rule_index_in_range(snap: &PermissionsSnapshot, index: usize) -> bool {
index < snap.permissions.rules.len()
} Try / catch
match remove_permission_rule(path, index, &token) {
Err(e) if e.to_string().contains("index changed before removal") => {
let snap = load_permissions_snapshot(path)?;
if index < snap.removal_tokens.len() {
remove_permission_rule(path, index, &snap.removal_tokens[index])?
}
}
other => other?,
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- agent profile tools.posture= would widen permissions; use…
- external credential access is disabled for
- failed to parse permissions at
- Failed to read MCP config
- parallel(): max 1000 items per call
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/6c9a942505b8358a.
Report an issue: GitHub.
Appendix: source
Thrown at crates/config/src/lib.rs:6355
}
toml_edit::Item::Value(value) => {
let Some(rules) = value.as_array_mut() else {
bail!("`rules` in permissions.toml must be an array");
};
rules.push(toml_edit::Value::InlineTable(permission_rule_inline_table(
rule,
)));
Ok(())
}
_ => bail!("`rules` in permissions.toml must be an array"),
}
}
fn remove_permission_rule_item(item: &mut toml_edit::Item, index: usize) -> Result<Option<String>> {
match item {
toml_edit::Item::ArrayOfTables(rules) => {
if index >= rules.len() {
bail!("permission rule index changed before removal");
}
let file_header = if index == 0 {
rules
.get(index)
.and_then(|rule| rule.decor().prefix())
.and_then(toml_edit::RawString::as_str)
.map(str::to_owned)
} else {
None
};
rules.remove(index);
if let Some(header) = file_header.as_deref()
&& let Some(next_rule) = rules.get_mut(0)
{
let next_prefix = next_rule
.decor()
.prefix()
.and_then(toml_edit::RawString::as_str)View on GitHub (pinned to 73e0f67d83)