Every maintainer knows the feeling. You open your repo's issue tracker and there are 847 open issues. Half of them are from 2022. Some reference APIs that no longer exist. A few are duplicates nobody caught. And buried somewhere in that mess are the three issues that actually matter this week.
Stale issues and PRs aren't just visual clutter — they actively slow down your team. New contributors can't tell what's relevant. Triaging becomes a full-time job. And that PR from eight months ago? It's now 200 commits behind main and would take longer to rebase than to rewrite.
Let's talk about how to automate the cleanup.
Why Manual Triage Doesn't Scale
I've tried the "issue bankruptcy" approach — closing everything older than 6 months and asking people to reopen if it's still relevant. It works once. Then six months later you're back in the same spot.
The core problem is that issues and PRs don't have a natural lifecycle. They get opened, maybe discussed for a day, then forgotten. Unlike branches (which you eventually merge or delete), issues just... persist.
Manual triage fails because:
- It requires consistent human attention on a recurring schedule
- Context about why something can be closed lives in people's heads
- The decision to close requires reading the full thread, checking if the bug still exists, or verifying if a feature was shipped another way
Automated Scanning With ClawSweeper
ClawSweeper is an open-source tool that tackles this by scanning all your issues and PRs on a weekly schedule. It analyzes each one and suggests what can be closed, along with the reasoning. Think of it as a triage assistant that never takes a day off.
The general approach works like this: the tool runs as a scheduled GitHub Action, iterates through open issues and PRs, evaluates them against a set of heuristics, and then either comments with a close recommendation or auto-labels items for review.
Setting Up Automated Issue Scanning
The pattern for scheduled issue scanning via GitHub Actions looks like this:
# .github/workflows/sweep-issues.yml
name: Sweep Stale Issues
on:
schedule:
# Run every Monday at 9am UTC
- cron: '0 9 * * 1'
workflow_dispatch: # Allow manual triggers for testing
jobs:
sweep:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Run issue scanner
uses: openclaw/clawsweeper@main
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
The workflow_dispatch trigger is something I always add — it lets you test the action without waiting a full week for the cron to fire.
What Makes an Issue "Closeable"?
The interesting part isn't the scheduling — it's the heuristics for deciding what's stale. Here are the signals that generally indicate an issue can be closed:
# Common heuristics for stale issue detection
def should_suggest_close(issue):
signals = []
# No activity in a long time
if days_since_last_comment(issue) > 90:
signals.append("no_recent_activity")
# Original author hasn't responded to questions
if has_unanswered_maintainer_question(issue) and days_waiting > 30:
signals.append("awaiting_response")
# Referenced PR was already merged
if linked_pr_merged(issue):
signals.append("fix_already_shipped")
# Issue references a version that's EOL
if references_eol_version(issue):
signals.append("outdated_version")
# Duplicate of another issue (based on content similarity)
if similarity_score(issue, open_issues) > 0.85:
signals.append("likely_duplicate")
return len(signals) >= 2, signals # Require multiple signals
The key insight here is requiring multiple signals before suggesting closure. A single signal (like "no activity in 90 days") catches too many false positives — some issues are legitimate feature requests that just haven't been prioritized yet.
Handling PRs Differently Than Issues
PRs have different staleness signals than issues. A PR that's been open for months usually means one of three things:
- The author lost interest
- It's blocked on a design decision nobody made
- It's drifted so far from main that it needs a complete rewrite
# Configuration for PR-specific rules
pr_rules:
max_days_without_update: 60
max_merge_conflicts: 5
check_ci_status: true
# Don't auto-suggest closing draft PRs — they're explicitly WIP
ignore_drafts: true
I'd recommend being more aggressive with PR cleanup than issue cleanup. A stale PR is almost never going to get merged as-is. The code has diverged, the review context is lost, and often the approach itself has been superseded.
Prevention: Reducing Future Staleness
Automated scanning catches the backlog, but you also want to reduce the inflow of issues that become stale:
- Use issue templates that require reproduction steps. Issues without repro steps are the ones that sit forever because nobody can verify them.
-
Label issues at creation time. A
needs-triagelabel makes it obvious what hasn't been looked at yet. - Set expectations in your CONTRIBUTING.md. Tell people that issues inactive for 90+ days may be closed, and that they can always reopen.
- Close PRs that fail CI for more than 2 weeks. If the author isn't fixing the tests, they're not coming back.
The "Suggest, Don't Auto-Close" Philosophy
One thing I appreciate about ClawSweeper's approach is that it suggests closures rather than auto-closing. I've seen too many bots that just slam issues shut with a generic "this issue has been inactive" message. It's hostile to contributors and it creates busywork when people reopen things.
The better workflow is:
- Bot comments with why it thinks the issue can be closed
- Maintainer reviews the suggestion (takes 5 seconds per issue vs. 2 minutes of manual triage)
- Maintainer either closes it or removes the label to indicate "keep open"
This keeps humans in the loop while eliminating the cognitive overhead of the initial scan.
Running It On Your Own Repos
If you maintain any repo with more than ~50 open issues, I'd recommend setting up some form of automated sweeping. Whether you use ClawSweeper specifically or build your own with the GitHub API and a scheduled action, the pattern is the same: scan weekly, suggest closures with reasoning, let humans make the final call.
The GitHub API gives you everything you need:
# Quick check: how bad is your backlog?
gh issue list --state open --json number,title,updatedAt \
--jq '[.[] | select(.updatedAt < "2025-01-01")] | length'
# Output: 142 (yikes)
If that number makes you uncomfortable, it's time to automate.
Final Thoughts
Stale issues aren't a moral failing — they're an inevitable consequence of active projects attracting more attention than any team can handle. The fix isn't "be more disciplined about triage" (that never sticks). The fix is tooling that does the boring scanning work for you, so you can spend your limited attention on the decisions that actually require human judgment.
Check out ClawSweeper on GitHub if you want a turnkey solution, or steal the heuristics above and build your own. Either way, your future self will thank you when the issue count drops below triple digits.
Top comments (1)
Good timing for me. I just open sourced a 65K line Rust
project this week and I'm already thinking about how to
keep the issue tracker clean from day one.
The "require multiple signals before suggesting closure"
point is smart. I've seen repos where the stale bot
closes everything after 30 days and it just annoys
people. Especially feature requests that are still valid
but nobody has time for yet.
One thing I'm doing early is keeping issue templates
strict. Reproduction steps required for bugs, clear
description required for features. It filters out the
low-effort issues before they pile up. Easier to enforce
that from the start than to add it after you already
have 200 open issues.
The PR cleanup advice is spot on too. A PR that's 60
days behind main is basically a new PR at that point.
Better to close it and let the author reopen fresh if
they still care.