GitHub Issues Management with Hermes Agent: gh and curl
Create, triage, label, assign GitHub issues via gh or REST.
Written by Neura Market from the official Hermes Agent documentation for Github Issues. Commands, paths, and version numbers are reproduced from the source unchanged.
Read the official documentationThis skill is for anyone who needs to create, triage, label, and assign GitHub issues from the command line, either interactively or as part of an automated workflow. It is bundled with Hermes Agent, so it is available by default, and it gives you two ways to work: the gh CLI when it is installed and authenticated, and plain curl against the GitHub REST API as a fallback. You would reach for this when you are inside a repository and need to move issues around without leaving the terminal, or when you want to script repetitive triage tasks.
What it does
The skill covers the full lifecycle of a GitHub issue: listing and searching, creating with structured bodies, editing labels and assignees, commenting, closing and reopening, linking issues to pull requests, and running bulk operations. Every capability is shown twice, once with gh and once with curl, so you are never blocked by missing tooling. The curl paths use the same REST endpoints that the GitHub web interface and official clients use, which means the skill also works as a reference for building your own API calls.
In practice, this is the skill you use when a bug report comes in and you need to apply a needs-triage label, assign it to the right person, and leave a note about your initial investigation. It is also the skill you use when you want to close a batch of stale issues that carry a wontfix label, or when you want to create a well-formed feature request from a template.
Before you start
The skill assumes two things are already in place. First, you are authenticated with GitHub. The github-auth skill handles that, and this skill's setup script checks for an existing gh login or a GITHUB_TOKEN in your environment. Second, you are inside a git repository that has a GitHub remote, because the setup script derives the owner and repository name from git remote get-url origin. If you are not in such a repository, you can still use the curl examples by substituting the owner and repo values manually, but the convenience of the automatic variables is lost.
The setup block below is the first thing the skill runs. It decides which authentication path to use and extracts the owner and repo names from the remote URL. If gh is available and authenticated, it uses that. Otherwise it falls back to git and looks for a GITHUB_TOKEN, first in the Hermes environment file, then in ~/.git-credentials via a helper script from the github-auth skill.
if command -v gh &>/dev/null && gh auth status &>/dev/null; then
AUTH="gh"
else
AUTH="git"
if [ -z "$GITHUB_TOKEN" ]; then
if _hermes_env="${HERMES_HOME:-$HOME/.hermes}/.env"; [ -f "$_hermes_env" ] && grep -q "^GITHUB_TOKEN=" "$_hermes_env"; then
GITHUB_TOKEN=$(grep "^GITHUB_TOKEN=" "$_hermes_env" | head -1 | cut -d= -f2 | tr -d '\n\r')
elif grep -q "github.com" ~/.git-credentials 2>/dev/null; then
GITHUB_TOKEN=$(uv run python "${HERMES_HOME:-$HOME/.hermes}/skills/github/github-auth/scripts/git-credential-token.py")
fi
fi
fi
REMOTE_URL=$(git remote get-url origin)
OWNER_REPO=$(echo "$REMOTE_URL" | sed -E 's|.*github\.com[:/]||; s|\.git$||')
OWNER=$(echo "$OWNER_REPO" | cut -d/ -f1)
REPO=$(echo "$OWNER_REPO" | cut -d/ -f2)
1. Viewing Issues
The first thing you will do with any issue tracker is look at what is already there. The gh commands cover the common cases: listing all issues, filtering by state and label, seeing what is assigned to you, and searching by text. The gh issue view 42 command shows the full detail of a single issue, including its body and comments.
With gh:
gh issue list
gh issue list --state open --label "bug"
gh issue list --assignee @me
gh issue list --search "authentication error" --state all
gh issue view 42
The curl equivalents hit the REST API directly. Notice the Python one-liners that parse the JSON response. The first listing filters out pull requests, because the GitHub API returns PRs in the same /issues endpoint. That is a common gotcha: if you forget the 'pull_request' not in i check, your issue list will be polluted with PRs.
With curl:
# List open issues
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/issues?state=open&per_page=20" \
| python -c "
import sys, json
for i in json.load(sys.stdin):
if 'pull_request' not in i: # GitHub API returns PRs in /issues too
labels = ', '.join(l['name'] for l in i['labels'])
print(f\"#{i['number']:5} {i['state']:6} {labels:30} {i['title']}\")"
# Filter by label
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/issues?state=open&labels=bug&per_page=20" \
| python -c "
import sys, json
for i in json.load(sys.stdin):
if 'pull_request' not in i:
print(f\"#{i['number']} {i['title']}\")"
# View a specific issue
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42 \
| python -c "
import sys, json
i = json.load(sys.stdin)
labels = ', '.join(l['name'] for l in i['labels'])
assignees = ', '.join(a['login'] for a in i['assignees'])
print(f\"#{i['number']}: {i['title']}\")
print(f\"State: {i['state']} Labels: {labels} Assignees: {assignees}\")
print(f\"Author: {i['user']['login']} Created: {i['created_at']}\")
print(f\"\n{i['body']}\")"
# Search issues
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/search/issues?q=authentication+error+repo:$OWNER/$REPO" \
| python -c "
import sys, json
for i in json.load(sys.stdin)['items']:
print(f\"#{i['number']} {i['state']:6} {i['title']}\")"
The search endpoint is separate from the list endpoint. The list endpoint filters within a repo, while the search endpoint supports full-text queries across the repo. The example searches for "authentication error" scoped to the current repo.
2. Creating Issues
Creating a well-formed issue is the first step in good bug tracking. The gh command takes a title and a body, and you can attach labels and assignees in the same call. The body in the example uses Markdown with sections for description, steps to reproduce, and expected behavior. That structure makes the issue readable and actionable.
With gh:
gh issue create \
--title "Login redirect ignores ?next= parameter" \
--body "## Description
After logging in, users always land on /dashboard.
## Steps to Reproduce
1. Navigate to /settings while logged out
2. Get redirected to /login?next=/settings
3. Log in
4. Actual: redirected to /dashboard (should go to /settings)
## Expected Behavior
Respect the ?next= query parameter." \
--label "bug,backend" \
--assignee "username"
The curl version posts a JSON payload to the issues endpoint. The body uses \n for newlines, which is the JSON way to represent a multi-line string. The labels and assignees are arrays.
With curl:
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues \
-d '{
"title": "Login redirect ignores ?next= parameter",
"body": "## Description\nAfter logging in, users always land on /dashboard.\n\n## Steps to Reproduce\n1. Navigate to /settings while logged out\n2. Get redirected to /login?next=/settings\n3. Log in\n4. Actual: redirected to /dashboard\n\n## Expected Behavior\nRespect the ?next= query parameter.",
"labels": ["bug", "backend"],
"assignees": ["username"]
}'
Bug Report Template
A good bug report is specific and reproducible. This template gives you a consistent structure to fill in. The environment section is often the difference between a fix that lands in minutes and one that takes days of back-and-forth.
## Bug Description
<What's happening>
## Steps to Reproduce
1. <step>
2. <step>
## Expected Behavior
<What should happen>
## Actual Behavior
<What actually happens>
## Environment
- OS: <os>
- Version: <version>
Feature Request Template
Feature requests benefit from a different structure. The motivation and alternatives sections force the requester to think about why the feature matters and what else was considered, which often surfaces simpler solutions.
## Feature Description
<What you want>
## Motivation
<Why this would be useful>
## Proposed Solution
<How it could work>
## Alternatives Considered
<Other approaches>
3. Managing Issues
Once an issue exists, you will spend most of your time editing it: adding labels, assigning people, commenting, and changing its state.
Add/Remove Labels
Labels are how you categorize issues at a glance. The gh commands use issue edit with --add-label and --remove-label. You can add multiple labels at once by comma-separating them.
With gh:
gh issue edit 42 --add-label "priority:high,bug"
gh issue edit 42 --remove-label "needs-triage"
The curl equivalents use the labels sub-resource. Adding labels is a POST to /issues/42/labels, removing one is a DELETE to /issues/42/labels/needs-triage. There is also a handy command to list all labels defined in the repository, which is useful when you are not sure what labels exist.
With curl:
# Add labels
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42/labels \
-d '{"labels": ["priority:high", "bug"]}'
# Remove a label
curl -s -X DELETE \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42/labels/needs-triage
# List available labels in the repo
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/labels \
| python -c "
import sys, json
for l in json.load(sys.stdin):
print(f\" {l['name']:30} {l.get('description', '')}\")"
Assignment
Assigning an issue makes the responsibility explicit. The gh command accepts a username or @me for the current user. The curl version posts to the assignees endpoint.
With gh:
gh issue edit 42 --add-assignee username
gh issue edit 42 --add-assignee @me
With curl:
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42/assignees \
-d '{"assignees": ["username"]}'
Commenting
Comments are how you communicate progress on an issue. The gh command is straightforward. The curl version posts to the comments endpoint. The example body uses an em dash, which is fine in a comment, but note that the JSON payload needs to be properly escaped if you have special characters.
With gh:
gh issue comment 42 --body "Investigated — root cause is in auth middleware. Working on a fix."
With curl:
curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42/comments \
-d '{"body": "Investigated — root cause is in auth middleware. Working on a fix."}'
Closing and Reopening
Closing an issue is a deliberate act. The gh command lets you add a reason, such as "not planned". The curl version uses a PATCH to the issue endpoint with a state field. The close example sets state_reason to completed, which is the default when you close via the web interface. Reopening is just a PATCH back to state: open.
With gh:
gh issue close 42
gh issue close 42 --reason "not planned"
gh issue reopen 42
With curl:
# Close
curl -s -X PATCH \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42 \
-d '{"state": "closed", "state_reason": "completed"}'
# Reopen
curl -s -X PATCH \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/42 \
-d '{"state": "open"}'
Linking Issues to PRs
GitHub has a built-in mechanism for closing issues when a pull request merges. You just include one of the keywords in the PR body. This is a convention, not a command, but it is part of the issue workflow because it automates the closing step.
Closes #42
Fixes #42
Resolves #42
To start work on an issue, you can create a branch directly from it. The gh command does this in one step and checks it out. The manual git equivalent is shown for when you do not have gh or prefer to do it yourself.
With gh:
gh issue develop 42 --checkout
With git (manual equivalent):
git checkout main && git pull origin main
git checkout -b fix/issue-42-login-redirect
4. Issue Triage Workflow
Triage is the process of going through new issues, understanding them, and deciding what to do. This skill gives you a repeatable workflow. The first step is to list issues that have the needs-triage label. The gh command is a simple filter. The curl version uses the same label filter on the list endpoint.
- List untriaged issues:
# With gh
gh issue list --label "needs-triage" --state open
# With curl
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/issues?labels=needs-triage&state=open" \
| python -c "
import sys, json
for i in json.load(sys.stdin):
if 'pull_request' not in i:
print(f\"#{i['number']} {i['title']}\")"
- Read and categorize each issue (view details, understand the bug/feature)
- Apply labels and priority (see Managing Issues above)
- Assign if the owner is clear
- Comment with triage notes if needed
Notice the numbered list in the source starts with a 1. for the first step, then continues with 1. again for the second step. That is a quirk of the source, but the intent is a sequential list. In practice, you would read each issue, decide if it is a bug or a feature, apply the appropriate labels, assign it to someone if the area is obvious, and leave a comment summarizing your triage decision.
5. Bulk Operations
When you have many issues to update, doing them one by one is tedious. The skill shows how to combine the list command with a loop. The gh version uses --json and jq to extract issue numbers, then pipes them to xargs to close each one. The curl version does the same with a while read loop.
With gh:
# Close all issues with a specific label
gh issue list --label "wontfix" --json number --jq '.[].number' | \
xargs -I {} gh issue close {} --reason "not planned"
With curl:
# List issue numbers with a label, then close each
curl -s \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$OWNER/$REPO/issues?labels=wontfix&state=open" \
| python -c "import sys,json; [print(i['number']) for i in json.load(sys.stdin)]" \
| while read num; do
curl -s -X PATCH \
-H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$OWNER/$REPO/issues/$num \
-d '{"state": "closed", "state_reason": "not_planned"}'
echo "Closed #$num"
done
These are destructive operations, so be careful. The gh version is more concise, but the curl version gives you more control over the output and error handling.
Quick Reference Table
The table below summarizes every action in this skill, showing the gh command and the corresponding REST endpoint. Keep it handy when you are scripting.
| Action | gh | curl endpoint |
|---|---|---|
| List issues | gh issue list | GET /repos/{o}/{r}/issues |
| View issue | gh issue view N | GET /repos/{o}/{r}/issues/N |
| Create issue | gh issue create ... | POST /repos/{o}/{r}/issues |
| Add labels | gh issue edit N --add-label ... | POST /repos/{o}/{r}/issues/N/labels |
| Assign | gh issue edit N --add-assignee ... | POST /repos/{o}/{r}/issues/N/assignees |
| Comment | gh issue comment N --body ... | POST /repos/{o}/{r}/issues/N/comments |
| Close | gh issue close N | PATCH /repos/{o}/{r}/issues/N |
| Search | gh issue list --search "..." | GET /search/issues?q=... |
When not to use it
The source does not list explicit alternatives, but the skill itself is not the right tool for every situation. If you are working in a repository that does not use GitHub, this skill is useless. If you need to manage projects or milestones, that is outside the scope of this skill. For heavy project management, you might want a dedicated tool like the GitHub web interface or a project management app. This skill is for quick, scriptable issue operations, not for complex workflow automation.
Limits and gotchas
The source does not list explicit limitations, but a few are implied by the setup and the API behavior. First, the curl examples assume GITHUB_TOKEN is set. If it is not, the setup script tries to find it in the Hermes environment file or in ~/.git-credentials, but if neither exists, the curl commands will fail with an authentication error. Second, the list endpoint returns pull requests as well as issues, so the Python filters are necessary to exclude them. Third, the per_page=20 parameter in the list examples means you only see the first 20 issues. For larger repositories, you would need to paginate, which the skill does not show. Fourth, the gh commands require the gh CLI to be installed and authenticated. If it is not, the skill falls back to curl, but you lose the convenience of gh's output formatting.
What pairs with this
This skill is part of a family of GitHub skills in Hermes Agent. It depends on github-auth for authentication, and it works naturally with github-pr-workflow for the pull request side of the cycle. After you triage an issue and create a branch with gh issue develop, you would use the PR workflow to open a pull request that references the issue and closes it on merge.