---
name: GitHub Lanes Bridge
slug: github-lanes-bridge
category: Automation
description: GitHub Lanes Bridge moves GitHub issues and PR updates into Lanes for local Claude Code sessions. It is used to import work, split issues into sub-issues, and post results back to GitHub.
github: "https://github.com/lanes-sh/app/tree/main/plugins/lanes-desktop/skills/github-lanes-bridge"
stars: 264
forks: 17
install: "npx degit https://github.com/lanes-sh/app/tree/main/plugins/lanes-desktop/skills/github-lanes-bridge ~/.claude/skills/github-lanes-bridge"
installs_to: ~/.claude/skills/github-lanes-bridge
source_path: plugins/lanes-desktop/skills/github-lanes-bridge/SKILL.md
collection_size: 4
category_size: 1648
collection_url: "https://dirskills.com/collections/lanes-sh/app"
added: 2026-09-02T05:20:05.440Z
last_synced: 2026-09-02T05:20:05.440Z
canonical_url: "https://dirskills.com/skills/github-lanes-bridge"
---

# GitHub Lanes Bridge

GitHub Lanes Bridge moves GitHub issues and PR updates into Lanes for local Claude Code sessions. It is used to import work, split issues into sub-issues, and post results back to GitHub.

**Install:**

```bash
npx degit https://github.com/lanes-sh/app/tree/main/plugins/lanes-desktop/skills/github-lanes-bridge ~/.claude/skills/github-lanes-bridge
```

## README

# GitHub ↔ Lanes bridge

Use this skill to move work between GitHub (where issues and PRs live) and Lanes (the local agent execution board). Typical flows: pull an issue into Lanes, run a session on it, then push the outcome (a PR-linking comment, or a new follow-up issue) back to GitHub.

This skill assumes the **`lanes-sessions`** skill is also available — it covers the lanes_* issue/session tools in detail. This skill focuses on the **bridging** layer.

When a Lanes issue hosts more than one CLI session, follow the multi-session disambiguation rules in `lanes-sessions` (pass `session` on stop / resume / read_terminal). Bridge tools (`lanes_github_*`) never address sessions directly.

`lanes_start_session` always **adds** a session, and its launch is asynchronous. In the batch flows below, call it exactly once per issue and record the slot it returns. Never re-call it because a session looks missing — an empty status read inside the launch window is expected. See "Starting is asynchronous" in `lanes-sessions`.

## When to use

- Work originates in GitHub and you want Claude Code to do the actual implementation locally.
- You want to fan out one GitHub issue into multiple Claude sessions on sub-tasks.
- You want PR links / progress updates to flow back to GitHub without leaving chat.

**Don't use** for ad-hoc one-off tasks that didn't start in GitHub; for those, just use Lanes directly.

## Prerequisites

- **Lanes MCP must be connected** — verify with `lanes_list_components`. If missing, run `/lanes:setup-mcp`.
- **GitHub must be connected inside Lanes** — verify with `lanes_github_list_repos`. If it returns a "not connected" error, ask the user to connect GitHub from **Lanes → Settings → GitHub** (OAuth flow). The token is read from `integrations.json` on every call. GitHub tokens don't expire, so no refresh logic to worry about.

There is no standalone GitHub MCP required. The same Lanes MCP that exposes `lanes_list_issues` also exposes `lanes_github_list_repos`, `lanes_github_get_issue`, etc. — they're additional tools on the same server.

## The GitHub tool surface

| Tool | Purpose |
|---|---|
| `lanes_github_list_repos` | List repos the user can access (owner, collaborator, or org member). Sorted by most-recently-pushed. Each is `{ id: "owner/name", name: "owner/name" }`. |
| `lanes_github_list_issues` | First 50 open issues in a repo, most-recently-updated first. PRs are filtered out automatically. |
| `lanes_github_search_issues` | Free-text search via the `/search/issues` endpoint. When the query is a pure number (`"#9"`, `"9"`), the exact-match issue is prepended. |
| `lanes_github_get_issue` | Single issue by **number** (passed as a string, e.g. `"42"`). |
| `lanes_github_create_issue` | Open a new issue. Labels are GitHub label **names** (not IDs). |
| `lanes_github_comment_on_issue` | Post a comment on an issue or PR. (GitHub uses the same endpoint for both.) |

Returned `ExternalIssue` shape:

```json
{
  "provider": "github",
  "externalId": "42",          // issue number as a string
  "externalKey": "#42",
  "externalUrl": "https://github.com/owner/name/issues/42",
  "title": "...",
  "description": "...",
  "state": "open",
  "updatedAt": 1715600000000
}
```

## Field mapping (GitHub → Lanes)

| GitHub | Lanes | Notes |
|---|---|---|
| `externalKey` (e.g. `#42`) | Title prefix + description marker | Used as the dedup key. |
| `title` | `title` | Pass through. |
| `body` (markdown) | `description` | Prepend a marker line: `GitHub: owner/name#42 — <externalUrl>` so round-tripping is trivial. |
| `state` (`open` / `closed`) | `step` | `open` → `backlog`/`todo` (your call); `closed` → `done` if explicitly importing closed issues. |
| upstream `externalId` (issue number) | `externalId` on `lanes_create_issue` | Pass through. |
| upstream `externalUrl` | `externalUrl` on `lanes_create_issue` | Pass through. |
| upstream `externalKey` | `externalKey` on `lanes_create_issue` | Pass through. |
| `labels` | `tags` | Different ID spaces. Look up Lanes label UUIDs via `lanes_list_labels` and map by name. Missing Lanes labels: ask user before creating. |

`lanes_create_issue` accepts `externalProvider`, `externalId`, `externalKey`, `externalUrl`, and `externalSyncedAt` — set all of them on import so the Lanes UI shows the badge and the refresh button works.

## Workflow patterns

### A. Single port (one GitHub issue → one Lanes session)

```
1. lanes_github_list_repos                                // (or user supplies "owner/name")
2. lanes_github_search_issues { repo: "owner/name", query: "#42" }
   or lanes_github_get_issue { repo, number: "42" }
3. lanes_list_issues { search: "owner/name#42" }          // dedup check
4. lanes_list_components                                  // pick componentId
5. lanes_create_issue {
     title: "owner/name#42 · <github title>",
     description: "GitHub: owner/name#42 — <externalUrl>\n\n<github body>",
     componentId,
     cwd,
     worktreeStrategy: "create",
     worktreeName: "gh/owner-name-42",
     externalProvider: "github",
     externalId: "42",
     externalKey: "#42",
     externalUrl: "<github url>",
     externalSyncedAt: <now epoch ms>
   }
6. lanes_start_session { issueId, planMode: true }
```

Confirm with the user before step 6 if the issue is non-trivial — let them review the issue first.

### B. Backlog import (filter GitHub → bulk Lanes issues)

```
1. lanes_github_list_issues { repo }   (capped at 50)
   or lanes_github_search_issues { repo, query: "label:p0" }
   Show the user the result count BEFORE importing.
2. Confirm scope with the user.
3. lanes_list_components, lanes_list_labels (cache locally)
4. For each GitHub issue:
     - dedup via lanes_list_issues { search: "<externalKey>" }
     - lanes_create_issue (with marker, mapped labels → tags, external* metadata)
5. (Optional) batch lanes_start_session in plan mode with
   flags: [{ flag: "dangerously-skip-permissions", value: "", kind: "flag" }]
```

For wider sweeps, prefer `lanes_github_search_issues` with explicit qualifiers (`label:foo`, `assignee:@me`, `milestone:"v2"`) over `list_issues` with no filter — GitHub's search syntax composes naturally inside the query string.

### C. Decomposition (one GitHub issue → many Lanes sub-issues)

```
1. lanes_github_get_issue { repo, number }                // big ticket
2. Propose a breakdown to the user (numbered list).
3. After user approves:
     a. lanes_create_issue for the parent (the umbrella, with GitHub marker).
     b. lanes_create_issue for each sub-task with
        dependencies: [parentLanesId].
4. lanes_start_session on the parent in plan mode (optional);
   start sub-tasks once their dependencies are unblocked.
```

If the team needs sub-issues visible upstream too, call `lanes_github_create_issue` for each and add their `#N` to each Lanes description.

### D. Round-trip (push results back)

When a session is done and there's something concrete to report:

```
1. lanes_get_issue_changes { id }                  // confirm there is a diff
2. (User pushes the branch / opens a PR.)
3. lanes_github_comment_on_issue {
     repo: "owner/name",
     number: "42",
     body: "Implemented locally via Lanes #<lanesId>. PR: <PR URL>\n\n<stat summary>"
   }
4. (Optional) lanes_github_create_issue for follow-up work surfaced during the
   session.
5. lanes_move_issue { id: lanesId, step: "done" }
```

PRs and issues share the comment endpoint on GitHub — you can use `lanes_github_comment_on_issue` to comment on a PR by passing its number.

## Idempotency

Always include `GitHub: owner/name#N` in the Lanes description (and ideally in the title prefix). Before importing, run `lanes_list_issues { search: "owner/name#N" }` and skip if a match is found. For backlog imports, do the dedup check per-issue, not just up-front, so partially-imported batches can resume cleanly.

## Common pitfalls

- ❌ **Passing the GitHub issue number as a JSON integer.** The tool expects a string (e.g. `"42"`, not `42`) so the same parameter shape works for arbitrary external-id forms.
- ❌ **Forgetting `external*` metadata on import.** Without them, the Lanes UI can't show the "open in GitHub" badge or refresh the description.
- ❌ **Using `lanes_github_list_issues` and expecting PRs.** The endpoint mixes issues and PRs, but the tool filters PRs out (matching the renderer). Use the GitHub UI or `gh pr list` for PRs — there's no `lanes_github_list_prs` tool today.
- ❌ **Passing GitHub label IDs to `lanes_github_create_issue`.** Labels are by **name** (string), not ID — GitHub's REST API expects names.
- ❌ **Skipping the dedup marker.** Without `GitHub: owner/name#N` in the description, a re-run double-imports the same ticket.
- ❌ **Auto-pushing branches or opening PRs without user consent.** Never do remote git operations in Pattern D without the user explicitly asking.
- ❌ **Posting back on every session start.** Round-trip happens at session *end* (work landed). Don't spam GitHub comments mid-run.
- ❌ **Assuming `lanes_github_*` are read-only.** `lanes_github_create_issue` and `lanes_github_comment_on_issue` write to upstream — always confirm with the user before invoking on production repos.
