gastownhall/beads · error
issue %s is already closed
Error message
issue %s is already closed
What it means
This error is returned by the NotClosed issue validator when an operation is attempted on an issue whose Status is 'closed'. The library treats closed issues as immutable endpoints of their lifecycle, so mutations (close, update, etc.) against them are rejected to preserve history integrity. The message includes the issue ID so the caller knows which issue is blocked.
Source
Thrown at internal/validation/issue.go:231
}
if issue.Status != types.StatusInProgress {
return nil
}
if poolAliases != nil && slices.Contains(poolAliases(), issue.Assignee) {
return nil
}
return fmt.Errorf("cannot reassign %s: held by %q (in_progress); coordinate with the holder (bd mail %s) — pass --force only if their claim is abandoned (crashed agent, expired lease), or use bd reclaim", id, issue.Assignee, issue.Assignee)
}
}
// NotClosed validates that an issue is not already closed.
func NotClosed() IssueValidator {
return func(id string, issue *types.Issue) error {
if issue == nil {
return nil
}
if issue.Status == types.StatusClosed {
return fmt.Errorf("issue %s is already closed", id)
}
return nil
}
}
// NotHooked validates that an issue is not in hooked status.
func NotHooked(force bool) IssueValidator {
return func(id string, issue *types.Issue) error {
if issue == nil {
return nil
}
if !force && issue.Status == types.StatusHooked {
return fmt.Errorf("cannot modify hooked issue %s (use --force to override)", id)
}
return nil
}
}
View on GitHub (pinned to 71377f2769)
Solutions
- Check the issue's current status before mutating (bd show <id> or the API) and skip if already closed
- If the operation is idempotent by design, filter out closed issues client-side before invoking the validator-backed call
- If you genuinely must modify a closed issue, reopen it first (set status back to open) then perform the mutation
- Treat the error as success in idempotent close workflows by matching on the 'is already closed' message
Example fix
// before
for _, id := range ids {
bd.Close(id) // fails on already-closed issues
}
// after
for _, id := range ids {
issue := getIssue(id)
if issue != nil && issue.Status != types.StatusClosed {
bd.Close(id)
}
} Defensive patterns
Strategy: validation
Validate before calling
func canClose(issue *types.Issue) bool {
return issue != nil && issue.Status != types.StatusClosed
} Type guard
func isOpen(issue *types.Issue) bool {
return issue != nil && issue.Status != types.StatusClosed
} Try / catch
if err := closeIssue(id); err != nil {
if strings.Contains(err.Error(), "is already closed") {
return nil // idempotent close
}
return err
} Prevention
- Always fetch and inspect issue status before lifecycle mutations
- Make close operations idempotent by skipping already-closed issues
- In batch jobs, filter out closed issues before processing
- Refresh issue data from the source before acting on cached IDs
When it happens
Trigger: Calling any operation that runs the NotClosed validator (e.g. bd close or status-changing updates) with the ID of an issue whose issue.Status == types.StatusClosed; passing a nil issue returns nil, so only a non-nil closed issue triggers it.
Common situations: Scripts that close issues in a loop without checking current status (double-close), retrying a close command after a partially-failed batch, concurrent agents/processes closing the same issue, or stale local IDs fetched before another process closed the issue.
Related errors
- no store is open for this workspace
- not found
- no absolute native user directory is available
- ExternalDoltConfig: set either Socket OR (Host, Port), not b
- ExternalDoltConfig: must set Socket or (Host, Port)
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/e6325f4f491e5341.
Report an issue: GitHub.