9.4 KiB
gh-issue-sync is a simple command line tool that can sync GitHub issues to
local Markdown files for offline editing, batch updates, and seamless
integration with coding agents and back up.
The idea here is that you can bring in the state of a bunch of GitHub issues locally so that you can refine them with an agent until you're satisfied and sync back up the changes. It can also be useful if you are working without internet for one reason or another and you want to have your issues readable locally.
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@latest
This installs the binary to $GOBIN (or $GOPATH/bin). Make sure it's 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-sync
Optionally 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 sync
Directory 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 pull
This 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:
# For Claude Code / Codex
gh-issue-sync write-skill --agent codex
# For Pi (shittycodingagent.ai)
gh-issue-sync write-skill --agent pi
# For Claude Desktop
gh-issue-sync write-skill --agent claude
# For OpenCode
gh-issue-sync write-skill --agent opencode
# For Amp or other agents using ~/.config/agents/
gh-issue-sync write-skill --agent generic
Use --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 the issue numbers come from github you can use temporary issue numbers
until then. T42 or TABC are all valid temporary issue IDs. But they need
to start with a "T" so that we know they are temporary ones. After synching
they are given new 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 bug
Sync Behavior
- New issues are created in
open/orclosed/based on their state - Existing issues are updated if unchanged locally
- Local changes are preserved (conflicts are reported, not overwritten)
- Original versions are stored in
.sync/originals/for conflict detection - Local-only 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
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 status
Create 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 --edit
Local 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 456
Alternatively, 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 push
The 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-comments
Issue 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 100% LLM generated. It is unclear if LLM generated code can be copyrighted.
- License: Apache-2.0