Skip to main content
Glama

ghark lets a Claude Code session respond to review on the pull requests it opens. When the reviewer comments, the session wakes, makes the changes, pushes, and replies, without being told to. It runs as a local stdio MCP server.

Each server process owns its subscriptions. No webhook endpoint, public host, or shared service is required. The process polls GitHub while the session is open. It stops when the session ends.

Requirements

  • Python 3.11 or later

  • A GitHub personal access token in GH_TOKEN or GITHUB_TOKEN

  • uv to run the server

  • Claude Code for channel delivery; other clients can use pending_events

The token needs read access to the repositories and pull requests you watch. Fine-grained tokens need Pull requests: read-only, Contents: read-only, and read access to checks and commit statuses.

Related MCP server: GitHub PR Reviewer MCP

Run

Install the project in editable mode:

uv sync

Add it to Claude Code as a local MCP server:

claude mcp add --scope user ghark -- uv run --directory /path/to/checkout ghark

To enable channel delivery in Claude Code, start the session with the channel server enabled:

claude --dangerously-load-development-channels server:ghark

Custom channels are a Claude Code preview feature. Without the channel flag, the tools still work and events remain available through pending_events.

For Codex, add a stdio server entry to ~/.codex/config.toml:

[mcp_servers.ghark]
command = "uv"
args = ["run", "--directory", "/path/to/checkout", "ghark"]

Codex does not receive MCP channel pushes. Ask it to call pending_events when you want to check for activity.

Tools

  • subscribe watches its pr argument. It accepts a GitHub PR URL or OWNER/REPO#N.

  • unsubscribe takes the same pr argument and stops watching it.

  • subscriptions lists this process's active PRs. It waits up to 30 seconds for startup discovery to finish.

  • pending_events returns unread events and reports any events dropped at the queue limit.

The server reports commits, reviews, comments, check status changes, merge state changes, and PR closure, merge, or reopen activity. It combines events from a PR into a short notification. Merged and closed PRs leave the watch list.

Review requests

An event is actionable when it is a review, issue comment, or review-thread comment by a reviewer, other than a bare approval. The reviewers are the logins in GHARK_REVIEWERS (comma-separated), or by default the token's owner.

Claude posts with the reviewer's token, so its replies carry the reviewer's login. The server instructions tell it to end every comment with Claude Code's signature, 🤖 Generated with [Claude Code](https://claude.com/claude-code). Signed comments are never actionable, so Claude's replies do not wake it.

Actionable events wake the session through the wake hook (see Hooks), which works where channels are disabled by policy. While review requests are unread, the server keeps a flag file, <state directory>/<session ID>.pending, naming their PRs; pending_events removes it. The hook waits for the flag and then wakes Claude, even when the session is idle. The flag is only a signal: pending_events remains the durable read path. A session with no review requests costs one model turn a day, when the waiter re-arms.

Restarted sessions

Subscriptions and unread events live in memory, and they end with the stdio server process. A resumed or restarted Claude Code session starts a new process. That process has no subscriptions.

On startup the server rediscovers the PRs that the session most plausibly owns. Nothing is written to disk. In the background, it finds the git checkout of the project directory and reads the GitHub repositories named by its remotes. It then subscribes to every open PR by the token's owner in one of those repositories whose head branch is the checked-out branch. Other local branches are ignored: in a busy repository they name many PRs, some stale, and every session would wake on every review. A session in a worktree finds the main checkout's branch, since CLAUDE_PROJECT_DIR names the main checkout; the subscribe hook covers PRs it opens. Each PR gets the normal baseline, so old activity is not replayed. The wake hook re-arms at every session start, so nothing is asked of the model.

The project directory is CLAUDE_PROJECT_DIR, then PWD, then the working directory. Claude Code sets CLAUDE_PROJECT_DIR for stdio servers. The working directory alone does not identify the project: uv run --directory /path/to/checkout moves it to the ghark checkout. Discovery is best effort. Failures are logged, and the server serves without the missing subscriptions. Set GHARK_DISCOVER=0 to turn it off.

Two sessions in the same repository both discover the same PRs. Each session wakes on review of the other session's PRs. Set GHARK_DISCOVER=0 for one of the sessions, or unsubscribe PRs that the session does not own.

Channel delivery is best effort. The pending queue holds up to 1,000 events; pending_events reports how many older events were dropped when that limit was reached.

Hooks

Three Claude Code hooks do what the model could forget, with nothing to report the miss.

  • ghark-hook-wake (SessionStart and PostToolUse on pending_events, both asyncRewake) waits for review requests and wakes Claude when they arrive. It starts with every session and again after every pending_events call, so the model only has to call pending_events when woken, as the wake message says. A second waiter for the same session exits at once and silently. Claude Code kills a hook at its timeout without telling Claude, so the waiter wakes Claude a few minutes before it: about once a day, Claude calls pending_events, which arms the next waiter. The waiter exits quietly five minutes after the session's ghark server is gone.

  • ghark-hook-signature (PreToolUse) denies Bash commands that post an unsigned PR comment or review. An unsigned reply from Claude counts as a review request, so Claude could wake itself and reply to its own comment in a loop. The denial tells Claude to append the signature line and retry.

  • ghark-hook-subscribe (PostToolUse) runs after a successful gh pr create (or gh pr new). It tells Claude to call subscribe with the URL the command printed.

Add them to ~/.claude/settings.json or to a project's .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(gh *)",
            "command": "uv run --directory /path/to/checkout ghark-hook-signature",
            "timeout": 30
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "uv run --quiet --directory /path/to/checkout ghark-hook-wake 86400",
            "asyncRewake": true,
            "timeout": 86400
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "mcp__ghark__pending_events",
        "hooks": [
          {
            "type": "command",
            "command": "uv run --quiet --directory /path/to/checkout ghark-hook-wake 86400",
            "asyncRewake": true,
            "timeout": 86400
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(gh *)",
            "command": "uv run --directory /path/to/checkout ghark-hook-subscribe",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

The if filter starts the Bash hooks only for gh commands, including each command in a && chain. The wake hook's argument must match its timeout.

The signature hook checks these commands:

  • gh pr comment, gh pr review, and gh issue comment with --body/-b or --body-file/-F

  • gh api POSTs to …/comments, …/reviews, …/reviews/N/events, and …/replies with body or …[body] fields, -F body=@file, or --input

  • gh api graphql comment and review mutations

It reads body files relative to the session's working directory. It allows any body it cannot read with confidence: a shell variable, a command substitution, a pipe into -F -, a file the same command writes first, or a command the shell lexer cannot parse. A false block stalls work; a rare miss costs one extra wake-up.

The subscribe hook covers PRs created during a session, not PRs opened before it started.

Development

uv run ghark

Check formatting and lint with:

uv run ruff format --check ghark
uv run ruff check ghark

Run the behavior and MCP stdio checks with:

uv run python -m unittest discover -s tests -v

Set GHARK_POLL_SECONDS to change the polling interval. The default is 30 seconds. Set GHARK_BATCH_SECONDS to change the event coalescing window. The default is 5 seconds. Set GHARK_DISCOVER=0 to disable startup discovery.

The server logs to stderr and to ~/.local/state/ghark/ghark.log (or $XDG_STATE_HOME/ghark/ghark.log). Set GHARK_LOG_FILE to change the path. Each line carries the process ID. The log records why each process stopped: the client closed the connection, a signal, or an uncaught exception. A log that ends without one of these means the process was killed with SIGKILL.

See DESIGN.md for architecture and behavior details.

Available Tools

4 tools
pending_eventsB

Read pending events and report any events dropped at the queue limit.

Inputs: no named parameters.

subscribeB

Watch a GitHub pull request for new activity.

Inputs: pr (required).

subscriptionsA

List pull requests watched by this session.

Inputs: no named parameters.

unsubscribeC

Stop watching a pull request.

Inputs: pr (required).

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.5/5.0

    Scored across 4 tools

    Disambiguation5/5

    Each tool targets a clearly distinct operation: subscribe and unsubscribe are opposite pair, subscriptions lists watches, and pending_events reads queued events. There is no realistic confusion between listing subscriptions and reading pending events, as they operate on different resources.

    Naming Consistency4/5

    Names are all snake_case and thematically coherent (subscribe/unsubscribe/subscriptions pair nicely). The only minor deviation is pending_events being a pure noun while the others read as verbs/plurals, but overall the pattern is predictable and readable.

    Tool Count4/5

    Four tools is well-matched to the narrow, focused purpose of watching pull requests for a session. It is on the lean side but each tool pulls its weight, so nothing feels redundant.

    Completeness4/5

    The watch lifecycle is fully covered: add (subscribe), remove (unsubscribe), list (subscriptions), and consume events (pending_events). Minor gaps exist around identifying the session or inspecting individual events, but the core workflow is complete.

    Related MCP Connectors

    Related MCP Servers