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

  1. Check the issue's current status before mutating (bd show <id> or the API) and skip if already closed
  2. If the operation is idempotent by design, filter out closed issues client-side before invoking the validator-backed call
  3. If you genuinely must modify a closed issue, reopen it first (set status back to open) then perform the mutation
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/e6325f4f491e5341. Report an issue: GitHub.