gh-issue-sync is a command line tool that syncs GitHub issues to local
Markdown files for offline editing, batch updates, and integration with
coding agents.
Pull issues locally, refine them until you are satisfied, and sync changes
back. Also useful for offline access to your issues.
Why?
When refining many issues at once, editing them on GitHub can be tedious.
It is easier to make changes locally and push them all at once. This is
particularly useful when using Claude Code or similar tools to refine
issues iteratively.
Agents can work with issues locally until you are ready to push. The tool
also supports creating new issues locally with temporary IDs that get
replaced with real issue numbers after pushing.
Overview
gh-issue-sync mirrors GitHub issues into a local .issues/ directory as
Markdown files with YAML front matter. Edit issues in your favorite editor,
create new issues locally, and push changes back to GitHub when ready.
Installation
Prerequisites:
- Go 1.21+
- GitHub CLI (
gh) installed and authenticated (gh auth login)
Install with Go
go install github.com/mitsuhiko/gh-issue-sync/cmd/gh-issue-sync@latestThis installs the binary to $GOBIN (or $GOPATH/bin). Make sure it is in your PATH.
Build from source
git clone https://github.com/mitsuhiko/gh-issue-sync.git
cd gh-issue-sync
go build -o gh-issue-sync ./cmd/gh-issue-syncOptionally move the binary to your PATH:
mv gh-issue-sync ~/.local/bin/
# or
sudo mv gh-issue-sync /usr/local/bin/Quickstart
# Navigate to your project
cd my-project
# Initialize issue sync (auto-detects repo from git remote)
gh-issue-sync init
# Pull all open issues from GitHub
gh-issue-sync pull
# View your local issues
ls .issues/open/
# Edit an issue
$EDITOR .issues/open/123-fix-login-bug.md
gh-issue-sync edit 123
# Push your changes
gh-issue-sync push
# Or sync both ways (push then pull)
gh-issue-sync syncDirectory Location
When you run gh-issue-sync init, the .issues directory is created next to
the .git directory (at the repository root), regardless of your current
working directory.
For other commands, gh-issue-sync searches for .issues by walking upward
from the current directory until it finds one or reaches a .git root. This
means you can run commands from any subdirectory within your project.
Environment Variable Override
Set GH_ISSUE_SYNC_DIR to explicitly specify the .issues directory location:
# Use a custom location
export GH_ISSUE_SYNC_DIR=/path/to/my-project/.issues
gh-issue-sync list
# Or inline
GH_ISSUE_SYNC_DIR=~/.issues/work-project gh-issue-sync pullThis is useful when:
- Working with multiple repositories
- Storing issues outside the repository
- Using a shared issues directory across projects
Agent Skill
This tool is designed to work with coding agents. Install the skill file so
your agent knows how to use gh-issue-sync:
gh-issue-sync write-skill --agent codex # Codex
gh-issue-sync write-skill --agent pi # For Pi
gh-issue-sync write-skill --agent claude # Claude Code
gh-issue-sync write-skill --agent opencode # OpenCode
gh-issue-sync write-skill --agent generic # Amp and othersUse --scope to choose between user-level (default) or project-level installation:
# Install to user home directory (default)
gh-issue-sync write-skill --agent codex --scope user
# Install to current project directory
gh-issue-sync write-skill --agent codex --scope project| Agent | User Scope | Project Scope |
|---|---|---|
codex |
~/.codex/skills/ |
.codex/skills/ |
pi |
~/.pi/skills/ |
.pi/skills/ |
claude |
~/.claude/skills/ |
.claude/skills/ |
opencode |
~/.config/opencode/skill/ |
.opencode/skill/ |
amp, generic |
~/.config/agents/skills/ |
.agents/skills/ |
To install to a custom location:
gh-issue-sync write-skill --output /path/to/skills/gh-issue-sync/You can also read or copy the skill file directly: skill/SKILL.md
Creating Local Issues
Since issue numbers come from GitHub, you can use temporary issue numbers
until then. T42 or TABC are valid temporary issue IDs. They must start
with "T" to mark them as temporary. After syncing, they receive real numbers
and all references are updated.
Sync Both Ways
Push and pull in a single command:
# Push local changes, then pull remote updates
gh-issue-sync sync
# Include closed issues
gh-issue-sync sync --all
# Filter by label
gh-issue-sync sync --label bugSync Behavior
This is how issues are synced:
On Pull
- New issues are saved to
open/orclosed/based on state - Existing issues are updated only if unchanged locally
- Local changes are preserved; conflicts are reported but not overwritten
- Deleted local files are restored from GitHub (if originals exist)
- Use
--forceto overwrite local changes
On Push
- Local issues (T1, T2, etc.) are created on GitHub
- After creation, files are renamed with real issue numbers
- References like
#T1in other issues are automatically updated - Missing labels and milestones are created automatically
- Changed issues are pushed; conflicts with remote changes are skipped
Conflict Detection
Original versions are stored in .issues/.sync/originals/ to enable
three-way merge conflict detection between local, original, and remote.
List Issues
List and filter local issues:
# List open issues
gh-issue-sync list
# Include closed issues
gh-issue-sync list --all
# Filter by label, assignee, author, milestone
gh-issue-sync list --label bug --assignee alice
# GitHub-style search query
gh-issue-sync list --search "error no:assignee sort:created-asc"The --search flag supports GitHub issue search syntax:
is:open,is:closed- Filter by statelabel:NAME- Filter by labelno:label,no:assignee,no:milestone- Filter by missing fieldassignee:USER,author:USER,milestone:NAME- Filter by fieldsort:created-asc,sort:created-desc- Sort results- Free text - Search in title and body (case-insensitive)
Check Status
See what's changed locally:
gh-issue-sync statusCreate New Issues
Create issues locally before pushing to GitHub:
# Create with a title
gh-issue-sync new "My new feature idea"
# Create and open in editor
gh-issue-sync new "Fix login bug" --edit
# Create with labels
gh-issue-sync new "Critical bug" --label bug --label urgent
# Create with just the editor (no title required)
gh-issue-sync new --editLocal issues get temporary IDs like T1, T2. When pushed, they become real
GitHub issues and files are renamed automatically.
Close and Reopen Issues
# Close an issue (marks for closing on next push)
gh-issue-sync close 123
# Close with a reason
gh-issue-sync close 123 --reason not_planned
# Reopen a closed issue
gh-issue-sync reopen 456Alternatively, move files manually:
- Move from
open/toclosed/to close - Move from
closed/toopen/to reopen
Pending Comments
You can queue a comment to be posted when pushing an issue. Create a file named
{number}.comment.md in the same directory as the issue:
# Create a pending comment for issue #42
echo "Updated the acceptance criteria based on PM feedback." > .issues/open/42.comment.md
# The comment will be posted on push
gh-issue-sync pushThe comment file is automatically deleted after successfully posting. This is
useful for agents or batch workflows that want to leave notes when updating issues.
To skip posting comments during push:
gh-issue-sync push --no-commentsIssue File Format
Each issue is a Markdown file with YAML front matter:
---
number: 123
title: Fix login bug on mobile Safari
labels:
- bug
- ios
assignees:
- alice
- bob
milestone: v2.0
type: Bug
state: open
state_reason:
synced_at: 2025-12-29T17:00:00Z
---
The body of the issue goes here!Front Matter Fields
| Field | Type | Description | Editable |
|---|---|---|---|
number |
int/string | Issue number or local ID (T1, T2) | No (managed) |
title |
string | Issue title | Yes |
labels |
string[] | Label names | Yes |
assignees |
string[] | GitHub usernames | Yes |
milestone |
string | Milestone name | Yes |
type |
string | Issue type (org repos only) | Yes |
projects |
string[] | Project names | Yes |
state |
string | open or closed |
Via folder |
state_reason |
string | completed or not_planned |
Yes |
parent |
int | Parent issue number | Yes |
blocked_by |
int[] | Blocking issue numbers | Yes |
blocks |
int[] | Issues this blocks | Yes |
synced_at |
datetime | Last sync time | No (managed) |
File Naming
Files are named {number}-{slug}.md where slug is derived from the title:
123-fix-login-bug.mdT1-new-feature.md
The slug is for readability only, the tool identifies issues by the number prefix.
Conflict Handling
gh-issue-sync uses three-way comparison to detect conflicts:
| Local | Original | Remote | Action |
|---|---|---|---|
| Same | Same | Same | No action |
| Changed | Same | Same | Push local changes |
| Same | Same | Changed | Pull remote changes |
| Changed | Same | Changed | Conflict – skip with warning |
When a conflict occurs:
- On pull: local changes are preserved, remote update is skipped
- On push: remote changes are detected, local push is skipped
- Use
--forceon pull to overwrite local changes
License
This code is entirely LLM generated. It is unclear if LLM generated code
can be copyrighted.
- License: Apache-2.0