Install
$ agentstack add skill-skyfox675-agents-skills-gh-issue-locking ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
gh-issue-locking: Claim, Coordinate, Release
Multiple orchestrator sessions — run by different humans, on different machines, at the same time — work the same issue queue. Without a claim protocol, two agents silently pick up the same issue, burn duplicate work, and open conflicting PRs. This skill is the claim protocol: how to lock an issue before dispatching work, how to recognize and respect (or reclaim) someone else's lock, and how to release cleanly on every exit path.
Related skills: pick what to claim with the gh-issue-filing and gh-issue-labels skills; dispatch the work with the dispatching-subagents skill; run the surrounding loop with the orchestrating-slots skill; drive the resulting PR with the driving-prs-to-merge skill.
Project bindings
These values are project policy, not protocol. The adopting project defines them in its own CLAUDE.md; this skill refers to them by placeholder.
| Binding | Meaning | Proven default | |---|---|---| | ` | org/repo for --repo flags (or omit when cwd is the checkout) | — | | | Branch agent PRs merge into | dev (default branch was main) | | | Repo-wide "an automation holds this" label | agent-claimed, color F0E68C — created by the gh-issue-labels bootstrap script | | | Stable greppable token in every lock comment | claude-lock: | | | Operator-held "humans only, never dispatch" label | do-not-dispatch — label-name comparisons in jq filters are case-sensitive; whatever casing the bootstrap script creates is the only casing filters may assume (the bundled script creates lowercase names) | | | Lock age threshold before reclaim is considered | 24h (tune to typical PR cycle time) | | | Where agent worktrees live | see the dispatching-subagents skill | | | Team real-time channel for urgent pings | Slack | | | The project's human-only AI-mention trigger | @claude` | | Operator logins | Informational roster of known operators | resolve dynamically — see below |
` and defaults carry no project semantics and can be adopted verbatim. The ` label itself is created by the gh-issue-labels bootstrap script — do not create it inline. If you rename either binding, rename it in every grep and snippet below — a marker that drifts from the greps makes every lock invisible to stale-detection.
The three coordination primitives
Every claimed issue carries exactly three signals, placed together. Each serves a different consumer at a different cost; dropping any one breaks that consumer.
- Assignee = the operator's GitHub login. The at-a-glance human signal. It is what
gh issue list --assignee @mefilters on, what the web UI shows as an avatar, and what other operators scan to find unclaimed work. Concurrent operators scope their slot math to--assignee @me; an issue with the wrong (or a lingering) assignee scrambles everyone's view. - `` = the coarse machine lock. Any orchestrator scanning the open queue sees "an automation is on this" from a cheap list query, without fetching N issues' comments.
- A lock comment with a machine-parseable HTML marker = the canonical record. It answers who/where/when in enough detail (operator, worktree, branch, timestamp) to do staleness arithmetic and rescue work. The HTML comment is invisible in the rendered GitHub UI, so the bullet list above it duplicates the fields for humans.
The compression to remember: the comment is the canonical record; the label is the signal; the assignee is the face.
Resolve operator identity dynamically
Once per session:
OPERATOR=$(gh api user --jq .login)
Use $OPERATOR everywhere: issue assignee, the operator= field in lock comments, PR assignee (sub-agents must self-assign the same login — see the dispatching-subagents skill), and --assignee @me scoping. Do not hardcode an operator roster in docs or scripts: rosters drift. One procedure file observed in practice listed a login that disagreed with the rest of its memory by one character — dynamic resolution from the auth context is the only value that cannot rot. A roster in project bindings is informational only.
Before you claim: the pre-claim check
Run this gate immediately before every lock, even if a queue scan minutes ago said the issue was free.
- Hold label. If the issue carries ``, stop. It is operator-held work deliberately reserved for humans. If an agent claimed one in error, stop the agent and revert the claim.
- Assignee must be empty. Dispatch-readiness labels lie — in one illustrative incident a
ready-to-dispatchlabel persisted after another operator claimed, producing four double-locked issues. The assignee field is the only authoritative claim gate; re-verify it is[]immediately before locking, because another operator may have claimed between your scan and now. - Grep for an existing lock marker:
gh issue view --repo --json labels,assignees,comments \
--jq '.comments[] | select(.body | contains("")) | .body'
Then decide three ways:
| State | Test | Action | |---|---|---| | Active | Marker ≤ ` old, **or** any age with a linked PR still open | Bail out. Report who holds it (operator=) and where (worktree/branch). Coordinate via ; never reclaim. | | **Stale** | Marker > ` old and no PR activity (linked PR closed/merged without release, or no PR at all) | Reclaim politely (below). | | No lock | No marker found | Proceed to claim. |
Staleness is a two-condition test: age AND PR state. A 3-day-old lock with a healthy open PR is ACTIVE — treating age alone as staleness recreates exactly the two-agents-on-one-issue race this system exists to prevent. The dual condition also kills zombie locks from crashed orchestrators whose PR died.
Placing a lock (atomic, and always before dispatch)
Lock first, dispatch second — always. A sub-agent already working an unlocked issue is invisible to every other operator's scan; the claim window must close before any work starts.
Bootstrap the ` label via the gh-issue-labels skill's scripts/bootstrap-labels.sh` — do not create it inline. The script uses the same color and description as the claim/release protocol expects.
Claiming is three actions that must all land — a partial lock (label without assignee, comment without label) breaks one of the three consumers and is invalid:
gh issue edit --add-label "" --add-assignee "$OPERATOR"
gh issue comment --body "$(cat \`
- branch: \`\`
- claimed at: \`\`
This lock auto-expires after of no PR activity. If you can confirm the session has ended, reclaim by removing the \`\` label AND the \`$OPERATOR\` assignee, then start work.
EOF
)"
The canonical marker line uses the placeholder (with the default binding this renders as ``):
This exact shape is the grep target. ` is the ` value bound per the dispatching-subagents skill.
Three pitfalls that have each produced broken locks in practice:
- Leave the heredoc delimiter unquoted (``, which breaks marker parsing and tells other operators nothing.
The lock comment is deliberately self-documenting: the expiry policy and exact reclaim mechanics are embedded in the lock itself, so a future agent or human who finds it needs no procedure file to act correctly.
Bundled-issue locking
When one dispatch covers several issues (a lead issue plus tightly-coupled siblings fixed in the same PR), do not duplicate the full lock everywhere — duplicated worktree/branch records drift. Instead:
- Lead issue: full three-signal lock as above.
- Each sibling: label + assignee as normal, plus a short comment whose marker points at the lead:
gh issue comment --body "$(cat ** — claimed as part of the same agent session; see the lock on #.
EOF
)"
Anyone doing staleness arithmetic on a sibling follows bundled_with to the lead and evaluates that lock; release of the lead releases the bundle (run the release steps on every bundled issue).
Batch claims: the race re-check
GitHub has no compare-and-swap, so batch claiming is optimistic-lock-then-verify. When claiming several issues in one flow (e.g. a "claim the next 5 open issues" trigger), another operator can lock the same issue between your read and your edit. So, immediately after placing each lock, re-fetch and check for active markers:
# Collect all lock-marker comments on this issue
all_markers=$(gh issue view --json comments \
--jq '[.comments[] | select(.body | contains("")) | .body]')
# For each operator, consider only their NEWEST marker (ignore markers older
# than the most recent "Session lock released" comment for that operator).
# If more than one operator has an active marker (placed within the current
# race window), earliest at= timestamp wins.
Earliest-at=-wins is a deterministic tiebreak both parties compute independently — no negotiation channel needed. Active-marker comparison: consider only the newest marker per operator and ignore any marker whose at= timestamp predates the most recent "Session lock released" comment from that operator (released locks leave their comment in place but are no longer active).
The re-check is required only in multi-claim flows; for a single-issue claim the read-then-edit window is short enough that the race is negligible, and adding the ceremony everywhere is waste. Skipping it in batch flows lets two operators silently double-claim.
Stale locks: polite reclaim
When the pre-claim check says STALE:
gh issue edit --remove-label --remove-assignee
gh issue comment --body "Reclaiming stale lock from (last activity ). Continuing work."
# then place a fresh lock with your own $OPERATOR per the standard procedure
Remove both the label and the prior assignee — each is read by a different scan, and a lingering assignee makes the issue look claimed forever. The explicit reclaim comment keeps the audit trail honest: who took over, from whom, why.
One thing reclaim cannot do: recover the prior work. The prior operator's worktree is local to their machine. Reclaim means re-dispatch fresh — do not waste time hunting for their in-progress branch.
Never reclaim an active lock, even to "speed things up." Recent timestamp plus an in-flight PR means another agent is working right now; reclaiming creates the exact race the system prevents. If your need is genuinely more urgent, post a comment on the issue describing the conflict — the other operator's orchestrator will surface it on its next tick — and/or ping ``.
Releasing locks
Every exit path has a release. The semantics are deliberately asymmetric — success keeps the assignee as credit, failure strips it for availability — and that asymmetry is load-bearing.
PR merged into `` — release AND explicitly close
GitHub's Closes #N keyword only auto-closes issues when the PR merges into the repo's default branch. If your agent PRs land on a non-default ``, every issue silently stays open after its fix merges: the queue fills with phantom work, other operators re-dispatch already-solved problems, and claim labels stick around. (In production this produced zombie recon dispatches on merged work, and one session missed the explicit close nine times before the operator caught it.) The orchestrator must close explicitly, three commands in order:
# 1. release the lock label (assignee RETAINED as completion credit)
gh issue edit --remove-label ""
# 2. close with the resolving PR recorded
gh issue close --reason completed --comment "Resolved by PR # (merged into \`\`)."
# 3. audit-trail unlock comment
gh issue comment --body "Session lock released — PR # merged."
Retaining the assignee on a completed issue is intentional: it is the at-a-glance record of who landed it. If your project merges straight to the default branch, the explicit close is unnecessary (auto-close fires) but steps 1 and 3 still apply.
PR closed WITHOUT merging — full release back to the queue
OPERATOR=$(gh api user --jq .login)
gh issue edit --remove-label "" --remove-assignee "$OPERATOR"
gh issue comment --body "Session lock released — PR # closed without merging; issue remains open."
Here the assignee must go too: if it lingers after a failed attempt, the issue looks claimed in every operator's --assignee scan and nobody picks it up next round. One field encodes both meanings — kept = credit, stripped = available.
Released on a blocker, blocker now resolved — re-lock fresh
Releasing when you hit a blocker (upstream dependency, operator hold, missing infra) was the right call: it returned the issue to the queue honestly. When the blocker clears and you pick the issue back up, do not resurrect or edit the old lock comment — run the full claim procedure again with a fresh comment and a fresh timestamp. Staleness arithmetic reads the newest marker; a re-dated old lock is indistinguishable from tampering, and a fresh one costs nothing.
Reconciliation sweep: lock expiry is not issue closure
`` expiry handles the lock label for crashed orchestrators, but does nothing about the underlying issue. The two lifecycles desynchronize after crashes: a stale lock can sit on an issue whose PR merged long ago. Any agent that finds one runs the full merged-PR three-command block above to reconcile — not just a label strip.
If the stale lock belongs to a different operator (the marker's operator= ≠ your login) and the PR is genuinely merged or long-closed, you may reconcile their lock — but the unlock comment must name whose lock you cleared ("Releasing stale lock held by `` — PR # merged ."). Silent cross-operator state mutation corrupts the audit trail.
Capacity collapse: release PR-less locks
When sub-agent capacity dies mid-flight (org usage limits, mass crashes), all concurrent agents fail near-simultaneously, leaving a mix of opened PRs and orphaned locks. As part of recovery: stop dispatching, then release every lock you hold whose issue has no open PR — those locks would otherwise block the queue until expiry. Locks with open PRs stay; the PR keeps them active by definition. (See the orchestrating-slots skill for the resume scheduling.)
Multi-operator etiquette
- Never assign another operator's login to your work. It scrambles their
--assignee @meview and their slot math. - Never manually merge or close another operator's PRs. Where every PR opens with auto-merge armed (see the driving-prs-to-merge skill), the merge fires when CI greens; manual intervention bypasses their merge discipline and races their orchestrator's close-on-merge follow-ups. If your project lacks auto-merge, the rule restates as: only the dispatching operator's side merges their own PRs.
- Never force-push to another operator's branch. Same ownership boundary.
- Peer review crosses operators freely; merging does not. Reviewing a PR dispatched by another operator is fine and welcome: the PR keeps their assignee, your review posts from your login, and that mismatch is by design. Just review — leave the merge to their automation.
- Duplicate issues: the later filer yields. Comment "Duplicate of #X — closing in favour of that." and close your own. Exception: if the original is low-quality, enrich the original and close yours as the dupe — preserve the better content, not the earlier timestamp.
- Queue backups: first noticer calls the hold. Send a hold message in `` AND post "Holding new dispatches pending queue clear" as a comment on the most-recent PR in flight. Dual-channel matters: the comment is durable and discoverable by an operator who missed the chat.
- The comment is the record; the ping is for speed. Routine coordination goes as comments on the relevant issue or PR — durable, greppable by other orchestrators, survives operator absence. Urgent coordination (e.g. "rolling back X") gets both: the comment for the record, the `` ping for latency. A ping alo
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: skyfox675
- Source: skyfox675/agents-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.