GitHunt
RO

rozsazoltan/gh-issue-sync

A github issue sync tool for working with issues locally

gh-issue-sync: sync GitHub issues locally and back

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.

Watch the demo video

Watch the demo video

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:

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 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-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:

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 others

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 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 bug

Sync Behavior

This is how issues are synced:

On Pull

  • New issues are saved to open/ or closed/ 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 --force to 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 #T1 in 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 state
  • label:NAME - Filter by label
  • no:label, no:assignee, no:milestone - Filter by missing field
  • assignee:USER, author:USER, milestone:NAME - Filter by field
  • sort: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/ to closed/ to close
  • Move from closed/ to open/ 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.md
  • T1-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 --force on pull to overwrite local changes

License

This code is entirely LLM generated. It is unclear if LLM generated code
can be copyrighted.

rozsazoltan/gh-issue-sync | GitHunt