SLOPSHOPPER

pr-links

Turns #123 in Claude's replies into a clickable link to that PR on GitHub.

newrowsprocess
v0.1.0no licenseupdated 2026-10-04AydinHassan/claude-mods/plugins/pr-links
A shopper browsing a rack in a slop shop
README

claude-mods

A Claude Code plugin marketplace with my mods.

Install

claude plugin marketplace add AydinHassan/claude-mods
claude plugin install pr-links@aydin-mods

Mods

pr-links

Turns #123 in Claude's replies into a clickable link to that pull request on GitHub.

The repository is read from git remote get-url origin in the session's working directory. Links point at /pull/123, which GitHub redirects to the issue when the number belongs to an issue.

Limits:

  • Only Claude's reply text is rewritten. Tool output, such as a gh pr list result, is left as it is.
  • Only GitHub remotes are supported. With any other remote the text is not changed.
  • Numbers inside code, existing links and URLs are left alone. A bare all-digit hex colour such as #333 outside code is linked like a PR number.
Source 2 files
hooks/register.ts 28 lines
1import type { Register } from 'claude-code'
2import { linkify, repoUrlFromRemote } from './linkify'
3
4export const register: Register = on => {
5  const repoUrls = new Map<string, Promise<string | undefined>>()
6
7  on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
8    if (!/#\d/.test(e.props.text)) {
9      return next(e)
10    }
11
12    const cwd = await $.session.cwd()
13    let repoUrl = repoUrls.get(cwd)
14
15    if (repoUrl === undefined) {
16      repoUrl = $.process
17        .run(['git', 'remote', 'get-url', 'origin'], { cwd })
18        .then(ran => (ran.exitCode === 0 ? repoUrlFromRemote(ran.stdout) : undefined))
19        .catch(() => undefined)
20      repoUrls.set(cwd, repoUrl)
21    }
22
23    const url = await repoUrl
24
25    return url === undefined ? next(e) : next({ ...e, props: { ...e.props, text: linkify(e.props.text, url) } })
26  })
27}
28
hooks/linkify.ts 17 lines
1// Code fences, inline code, existing markdown links and bare URLs are matched
2// first so a number inside them is left alone.
3const PATTERN =
4  /(```[\s\S]*?(?:```|$)|~~~[\s\S]*?(?:~~~|$)|`[^`\n]*`|\[[^\]\n]*\]\([^)\n]*\)|https?:\/\/\S+)|(?<![\w&#/])#(\d{1,6})\b/g
5
6export function linkify(text: string, repoUrl: string): string {
7  return text.replace(PATTERN, (match, skipped: string | undefined, number: string) =>
8    skipped === undefined ? `[#${number}](${repoUrl}/pull/${number})` : match,
9  )
10}
11
12export function repoUrlFromRemote(remote: string): string | undefined {
13  const found = /github\.com[:/]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?\s*$/.exec(remote)
14
15  return found === null ? undefined : `https://github.com/${found[1]}/${found[2]}`
16}
17