Skip to content

Automate Locking Closed Issues and Pull Requests on GitHub

Locking a closed thread prevents new comments from being added. It keeps old threads quiet and lets maintainers direct their attention toward work that is still active. This post covers two approaches: a shell script using the GitHub CLI for one-time or ad hoc locking across one or more repositories, and a GitHub Actions workflow using dessant/lock-threads for ongoing automation.

Why Lock Closed Threads

When an issue or pull request is closed, the conversation is usually finished. The bug is fixed, the feature is shipped, the decision is made. But the thread stays open for comments by default, and it tends to attract a particular kind of follow-up.

The most common patterns are:

  • Status requests on resolved issues. Someone discovers a closed bug report months after the fix shipped, posts "is this resolved?", and generates a notification for every subscriber. The answer is in the issue title: it is closed.
  • "Same here" comments on closed issues. A contributor runs into a similar problem, finds the closed issue, and posts to the thread rather than opening a new one. The maintainer sees the notification, reads the comment, and has to decide whether the new report is the same issue or a related one. It is rarely the same issue.
  • Requests to reopen. Not all of these are unreasonable, but most closed threads that receive reopen requests have been closed intentionally. The request does not provide new information; it just generates overhead.
  • Off-topic activity. Old, high-visibility threads (particularly ones that appeared in search results for a popular error message) sometimes become drop boxes for tangentially related questions. None of this benefits the project.

Each of these generates a notification. Notifications cost attention. On a project with hundreds or thousands of closed threads, the cumulative drag is real.

Locking threads after they have been closed for a defined period draws a clear boundary. Old discussions are preserved and readable; they just cannot receive new replies. Contributors who encounter a locked thread are prompted to open a new issue if they have a related but distinct problem, which is almost always the right outcome.

The Script

For a one-time cleanup or ad hoc use, a shell script using the GitHub CLI handles bulk locking directly from the command line.

Prerequisites

You need two tools installed and available on your PATH.

Install the GitHub CLI

brew install gh
sudo apt update && sudo apt install -y gh
sudo dnf install -y gh
winget install GitHub.cli

After installation, authenticate with GitHub:

gh auth login

Run gh auth status to confirm your session is active before continuing.

Install jq

brew install jq
sudo apt update && sudo apt install -y jq
sudo dnf install -y jq
winget install jqlang.jq

The Script

Test Before Running at Scale

Before running this script against any production repository, test it on a private repository you control with a small number of closed issues and pull requests. Locking a thread is reversible (you can unlock it via the UI or API), but running the script at scale against the wrong repository is easy to do and harder to recover from.

This script is provided as-is with no warranties and confers no rights. You are responsible for validating it in your own environment before use.

#!/bin/bash

# Array of repositories in the format "owner/repo"
repos=(
    "owner/foo"
    "owner/bar"
    "owner/baz"
)

# Function to check if issues are enabled in a repository
issues_enabled() {
    local repo=$1
    gh api -H "Accept: application/vnd.github.v3+json" "/repos/$repo" | jq -r '.has_issues'
}

# Function to check if an issue is locked
is_issue_locked() {
    local repo=$1
    local issue_number=$2
    gh api -H "Accept: application/vnd.github.v3+json" "/repos/$repo/issues/$issue_number" | jq -r '.locked'
}

# Function to check if a pull request is locked
is_pr_locked() {
    local repo=$1
    local pr_number=$2
    gh api -H "Accept: application/vnd.github.v3+json" "/repos/$repo/pulls/$pr_number" | jq -r '.locked'
}

# Function to lock all closed issues and pull requests in a repository
lock_closed_issues_and_prs() {
    local repo=$1
    echo "Locking closed issues and pull requests in $repo..."

    if [ "$(issues_enabled "$repo")" == "true" ]; then
        # Lock all closed issues
        closed_issues=$(gh issue list -R "$repo" --state closed --json number --jq '.[].number')
        for issue in $closed_issues; do
            if [ "$(is_issue_locked "$repo" "$issue")" == "false" ]; then
                gh issue lock "$issue" -R "$repo" --reason resolved
                echo "Locked closed issue #$issue in $repo"
            fi
        done
    else
        echo "Issues are not enabled in $repo"
    fi

    # Lock all closed pull requests
    closed_prs=$(gh pr list -R "$repo" --state closed --json number --jq '.[].number')
    for pr in $closed_prs; do
        if [ "$(is_pr_locked "$repo" "$pr")" == "false" ]; then
            gh pr lock "$pr" -R "$repo" --reason resolved
            echo "Locked closed pull request #$pr in $repo"
        fi
    done
}

# Iterate over each repository and lock closed issues and pull requests
for repo in "${repos[@]}"; do
    lock_closed_issues_and_prs "$repo"
done

echo "All closed issues and pull requests have been locked in the specified repositories."

Set each entry in the repos array to the full owner/repo path for every repository you want to process. The script skips issues in repositories where the Issues feature is disabled and skips any thread that is already locked, so it is safe to run more than once.

How It Works

  1. For each repository in the repos array, the script calls issues_enabled to confirm whether the Issues feature is active on that repository before attempting to list issues.
  2. gh issue list and gh pr list retrieve all closed issues and pull requests, returning only the number field as a list.
  3. For each number, the script checks the current lock status. If the thread is already locked, it is skipped.
  4. gh issue lock and gh pr lock lock the thread with the resolved reason, which is the most appropriate reason for closed items.

Targeting Multiple Repositories

To run the script across several repositories at once, add each one to the repos array:

repos=(
    "owner/foo"
    "owner/bar"
    "owner/baz"
)

The script iterates over the full list, so adding repositories is the only change needed to extend its scope.

Tip

The same pattern used here (iterating through gh CLI results and applying a bulk operation) also applies to other housekeeping tasks. The post on bulk-deleting GitHub Actions workflow runs covers a similar approach for clearing accumulated run history.

Automating with GitHub Actions

The script handles one-time or ad hoc runs well. For repositories that are actively receiving closed issues and pull requests on an ongoing basis, automating the locking process is more practical. The dessant/lock-threads action runs on a schedule and locks threads that have been closed for a configurable number of days.

The Workflow

---
name: Lock

on:
  schedule:
    - cron: "30 0 * * *"
  workflow_dispatch:

permissions:
  contents: read

jobs:
  lock:
    name: Lock Threads
    runs-on: ubuntu-latest
    permissions:
      issues: write
      pull-requests: write
      discussions: write
    steps:
      - name: Lock Threads
        uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          issue-comment: >-
            I'm going to lock this issue because it has been closed for 30
            days. This helps our maintainers find and focus on the active
            issues.

            If you have found a problem that seems similar to this,
            please open a new issue and complete the issue template so we can
            capture all the details necessary to investigate further.
          issue-inactive-days: "30"
          pr-comment: >-
            I'm going to lock this pull request because it has been closed for
            30 days. This helps our maintainers find and focus on the active
            issues.

            If you have found a problem that seems related to this change,
            please open a new issue and complete the issue template so we can
            capture all the details necessary to investigate further.
          pr-inactive-days: "30"

Save this file to .github/workflows/lock.yml in your repository. The workflow runs daily at 00:30 UTC and can also be triggered manually from the Actions tab using workflow_dispatch.

Understanding the Configuration

issue-inactive-days and pr-inactive-days control how long a closed thread must have been inactive before it is locked. The example above uses "30", so any issue or pull request closed for 30 or more days without recent activity is a candidate for locking.

issue-comment and pr-comment set the message posted to the thread before it is locked. The comment communicates the reason for locking and directs contributors toward the right path if they have a related problem. The >- syntax in YAML produces a single folded string; blank lines within the block become paragraph breaks in the rendered comment.

Using the GITHUB_TOKEN for authentication scopes the action to the repository where the workflow runs. No additional secrets or tokens are required.

Adding Discussion Support

The lock-threads action supports GitHub Discussions in addition to issues and pull requests. To include discussions in the locking policy, add the discussion-specific inputs alongside the issue and pull request ones:

      - name: Lock Threads
        uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          issue-inactive-days: "30"
          pr-inactive-days: "30"
          discussion-inactive-days: "30"
          discussion-comment: >-
            I'm going to lock this discussion because it has been closed for
            30 days. This helps our maintainers find and focus on the active
            discussions.

            If you have a related question or a new topic to raise, please
            open a new discussion.

The discussions: write permission in the job's permissions block is already present in the workflow above, so no change to the permissions configuration is needed to enable this.

Choosing Between the Two Approaches

The script and the action address different situations.

Use the Script when you need to lock a large backlog of already-closed threads in one pass. If a repository has been active for years without a locking policy, there may be hundreds or thousands of closed threads that have never been locked. Running the script once clears that backlog. It also works well for locking across a specific list of repositories in one operation, without needing to add the workflow file to each one individually.

Use the Action when you want ongoing automation. Once the workflow is committed, newly closed threads are locked automatically after the configured inactivity period. There is nothing to run manually, and the policy is version-controlled alongside the rest of the repository.

For a repository that has been around for a while and is transitioning to a locking policy, the most practical approach is to do both: run the script once to lock the existing backlog, then add the workflow to handle everything going forward.