Hmbown/CodeWhale · error
snapshots carry one removal token per rule
Error message
snapshots carry one removal token per rule
What it means
Panic from `snapshot.removal_token(index)` in `remove_permission`. The code asserts an internal invariant: every rule present in the permission snapshot has a corresponding removal token. If the rules list and token list ever disagree in length, the lookup returns None and the expect panics.
Solutions
- Fix snapshot construction so removal tokens are generated for every rule
- Check `snapshot.rules().len()` equals the token count before indexing
- Replace the expect with graceful handling returning `rule_not_found` if tokens can legitimately be absent
Example fix
// before
let token = snapshot
.removal_token(index)
.expect("snapshots carry one removal token per rule");
// after
let Some(token) = snapshot.removal_token(index) else {
return rule_not_found(app, display_index);
}; Defensive patterns
Strategy: type-guard
Validate before calling
debug_assert_eq!(snapshot.rules().len(), snapshot.removal_token_count(), "rules and removal tokens out of sync");
Type guard
fn removal_token(snapshot: &Snapshot, index: usize) -> Option<String> { snapshot.removal_token(index) } Try / catch
let Some(token) = snapshot.removal_token(index) else { return rule_not_found(app, display_index); }; Prevention
- Keep token generation in the same constructor as rules so they can't diverge
- Add a unit test asserting every rule index has a token
- Avoid concurrent mutation of permission state between snapshot and command
When it happens
Trigger: Invoking `/permissions remove <index>` where the snapshot at that index has no removal token — i.e., snapshot rules and removal tokens were built out of sync when the snapshot was constructed.
Common situations: A code change added a rule source without extending removal-token generation; concurrent mutation of permissions between snapshot creation and removal command handling.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- event recovery buffer fits u64
- event transaction runs once
- provider authority checked above
- Runtime store root
- turn monitor exits normally only after assigning a terminal…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/be113472796dfc33.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/commands/groups/config/permissions.rs:55
}
let Ok(display_index) = parts[1].parse::<usize>() else {
return usage_error(app);
};
let Some(index) = display_index.checked_sub(1) else {
return rule_not_found(app, display_index);
};
if parts.len() == 2 {
let snapshot = match load_snapshot(app) {
Ok(snapshot) => snapshot,
Err(error) => return operation_error(app, &error),
};
let Some(rule) = snapshot.rules().get(index) else {
return rule_not_found(app, display_index);
};
let token = snapshot
.removal_token(index)
.expect("snapshots carry one removal token per rule");
let command = format!("/permissions remove {display_index} --confirm {token}");
let rule = format_rule(app, display_index, rule);
let message = tr(app.ui_locale, MessageId::PermissionsRemovePreview)
.replace("{index}", &display_index.to_string())
.replace("{rule}", &rule)
.replace("{command}", &command);
return CommandResult::message(message);
}
if !parts[2].eq_ignore_ascii_case("--confirm") || parts[3].is_empty() {
return usage_error(app);
}
let removed =
match codewhale_config::remove_permission_rule(app.config_path.clone(), index, parts[3]) {
Ok(rule) => rule,
Err(error) => return operation_error(app, &error),
};
let message = tr(app.ui_locale, MessageId::PermissionsRemoved)View on GitHub (pinned to 73e0f67d83)