Shows the repo and branch Claude is working in, and names new sessions from their first prompt

Six small mods for Claude Code, written in TypeScript against its plugin hooks API. They add guard rails on tool calls, status line items, panes and slash commands: blocking risky git and shell commands, tracking your pull requests, gathering a standup, pacing session limits, showing which repo Claude is in, and keeping the Mac awake while work runs. Each mod is its own plugin, so you can install only the ones you want.
Add the marketplace from GitHub or from a local clone:
/plugin marketplace add AbdAlrahmanAlQaroot/claude-code-mods
/plugin marketplace add ~/path/to/claude-code-mods
Then install the mods you want:
/plugin install rules-guard@claude-code-mods
/plugin install pr-radar@claude-code-mods
The same works from a shell with claude plugin marketplace add <source> and claude plugin install <name>@claude-code-mods. Options are set with /plugin (pick the plugin, then configure) or claude plugin configure <name>. Restart the session after installing or changing options.
pr-radar and standup call the GitHub CLI, so gh must be installed and logged in. stay-awake and the pr-radar desktop notification are macOS only.
Checks Bash, Write and Edit calls before they run and holds the ones that break a rule. Each block names the rule, the problem, where it was found, the fix, and when to ask the user, so Claude can correct itself and retry.
Always on:
--all, --mirror and a bare git push from a protected branch. A bare push whose branch cannot be told is held until the branch is named.--force-with-lease is fine.Optional (off by default):
noAiAttribution: block AI attribution lines (co-author trailers, session links, "generated with" lines) in commits, PRs, comments and files.noTestPlan: block a "Test plan" section in PR descriptions.prBulletsOnly: PR descriptions must be 20 lines or fewer of short bullet points, with no prose paragraphs.noCodeComments: block new code comments in source files inside a git repo. Tool directives such as @ts- or eslint- lines and shebangs are allowed.noTestFiles: block a git commit that would include test files.noSlackSend: block Claude from sending or scheduling Slack messages. Drafts are allowed.liquibaseAuthor: the only author allowed on Liquibase changesets. Empty turns the check off.vpnProcess: process name that runs while your VPN is up, such as openvpn. When set, kubectl calls are held while it is not running.awsSsoCheck: hold kubectl and aws calls when no AWS SSO session in ~/.aws/sso/cache is fresh.Other options:
protectedBranches: comma-separated branch names. Default main,master.Keeps the state of your open GitHub pull requests across every repo: review decision, CI checks, unresolved threads, conflicts, and PRs merged in the last three days. It shows a summary in the status line, refreshes every five minutes and after gh pr or git push commands, and tells you when a PR is approved, gets changes requested, starts or stops failing, gets new comments, merges or closes.
/prs lists your PRs with links. /prs pane opens a live pane. /prs refresh fetches now./unreleased compares main (or master) with your integration branch in each repo and lists the PRs and commits not yet released.my_prs and my_unreleased, and prompts about PRs carry the live state as context.Options:
alerts: toast and macOS notification on PR changes. Default true.sound: play a sound with the notification. Default true.repos: comma-separated owner/repo list for /unreleased. Default empty, which uses only repos where you have a PR into the integration branch.integrationBranch: the branch work merges into before a release. Default staging.login: your GitHub login, used to mark your own commits in /unreleased. Default empty, which reads it from gh api user./standup opens a pane with what you did on a work day: PRs you opened, merged and reviewed, the commits you made, and the Claude Code sessions you worked in, grouped by repo. With no argument it shows the previous work day. It also accepts today, yesterday or a date like 2026-10-06. The Draft button asks Claude to write the update.
Options:
repos: comma-separated local repo paths to read commits from, including unpushed branches. Paths can be absolute, start with ~/, or be relative to your home folder. Default empty, which uses GitHub search only.utcOffset: your time zone as an offset from UTC, such as +03:00. Default +00:00.workDays: comma-separated work days. Default Mon,Tue,Wed,Thu,Fri.draftCommand: a slash command the Draft button runs with the day as its argument. Default empty, which sends Claude a prompt with the day's activity instead.Shows session-limit usage in the status line with the time until each window resets, and shows a toast once at 80% and once at 95% of each window.
Options:
sonnetSubagentsAbove: when usage of any window is above this percent, general-purpose subagents with no model of their own run on Sonnet. Default 0, which turns it off.handoffAt: when the 5-hour window reaches this percent, run your own /handoff command once in the session you are actively working in. Default 0, which turns it off.Shows the repo and branch Claude is working in on the status line, following cd and git -C in its commands. It also names a new session from its first real prompt, using a short Haiku call.
No options.
Keeps the Mac from idle-sleeping while a turn or a background agent runs, using caffeinate, and lets it sleep again once everything is done.
No options.
MIT. See LICENSE.
hooks/register.ts 113 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { RepoSpot } from '../types'
5
6const FRESH_MS = 30_000
7const PATH = String.raw`(?:"([^"]+)"|'([^']+)'|([^\s;&|()]+))`
8const LEADING_CD = new RegExp(String.raw`^\s*(?:cd|pushd)\s+${PATH}`)
9const GIT_DASH_C = new RegExp(String.raw`\bgit\s+-C\s+${PATH}`)
10const MAY_MOVE_BRANCH = /\b(git|gh)\b/
11const SHORT_REPLY = /^(y|yes|n|no|ok|okay|sure|go|go on|do it|continue|retry|again|done|thanks|thank you)\W*$/i
12const NOISE = /\[(?:Image|Pasted text)[^\]]*\]|https?:\/\/\S+/g
13
14const spot = atom({ plugin: 'repo-context', key: 'spot' } as const, null as RepoSpot | null)
15const titled = atom({ plugin: 'repo-context', key: 'titled' } as const, false)
16
17const pathIn = (command: string, pattern: RegExp) => {
18 const found = pattern.exec(command)
19 return found === null ? undefined : (found[1] ?? found[2] ?? found[3])
20}
21
22const resolve = (path: string, base: string, home: string | undefined) => {
23 if (home !== undefined && (path === '~' || path.startsWith('~/'))) return home + path.slice(1)
24 if (path.startsWith('/')) return path
25 return `${base.replace(/\/+$/, '')}/${path}`
26}
27
28const probe = async ($: EngineInterface, dir: string) => {
29 const head = await $.process.run(['git', '-C', dir, 'rev-parse', '--show-toplevel', '--abbrev-ref', 'HEAD'], { timeoutMs: 5000 })
30 if (head.exitCode !== 0) return undefined
31 const [top = '', branch = ''] = head.stdout.trim().split('\n')
32 const repo = top.split('/').filter(Boolean).at(-1)
33 if (repo === undefined || branch === '') return undefined
34 if (branch !== 'HEAD') return `${repo}:${branch}`
35 const sha = await $.process.run(['git', '-C', dir, 'rev-parse', '--short', 'HEAD'], { timeoutMs: 5000 })
36 return `${repo}:detached@${sha.stdout.trim()}`
37}
38
39const follow = async ($: EngineInterface, command: string, before: string) => {
40 const home = await $.env.get('HOME')
41 const named = pathIn(command, GIT_DASH_C) ?? pathIn(command, LEADING_CD)
42 const dir = named === undefined ? await $.session.cwd() : resolve(named, before, home)
43 const now = await $.clock.now()
44 const last = await read($, spot)
45 const isFresh = last !== null && last.dir === dir && now - last.at < FRESH_MS
46 if (isFresh && !MAY_MOVE_BRANCH.test(command)) return
47 const label = await probe($, dir)
48 if (label === undefined) return
49 await update($, spot, () => ({ dir, label, at: now }))
50 if (last?.label !== label) $.ui.status(label)
51}
52
53const fallbackTitle = (prompt: string) =>
54 prompt
55 .replace(NOISE, ' ')
56 .replace(/[^\p{L}\p{N}\s/#.:_-]/gu, ' ')
57 .split(/\s+/)
58 .filter(Boolean)
59 .slice(0, 6)
60 .join(' ')
61
62const cleanTitle = (text: string) =>
63 (text.trim().split('\n')[0] ?? '')
64 .replace(/^[\s"'`*#]+|[\s"'`*.!?]+$/g, '')
65 .split(/\s+/)
66 .slice(0, 6)
67 .join(' ')
68 .slice(0, 60)
69
70const nameSession = async ($: EngineInterface, prompt: string) => {
71 const reply = await $.model.complete({
72 model: 'haiku',
73 system: 'You name coding sessions. Reply with a 3 to 6 word title in plain words and nothing else: no quotes, no trailing punctuation.',
74 prompt: `Name the session that starts with this request:\n\n${prompt.slice(0, 2000)}`,
75 maxTokens: 30,
76 effort: 'low',
77 timeoutMs: 6000,
78 })
79 if (!reply.isAnswered) return undefined
80 const title = cleanTitle(reply.text)
81 return title === '' ? undefined : title
82}
83
84export const register: Register = on => {
85 on('session.start', async ($, e, next) => {
86 const started = await next(e)
87 const last = await read($, spot)
88 if (last !== null) $.ui.status(last.label)
89 return started
90 })
91
92 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
93 if (e.agentId !== undefined) return next(e)
94 const before = await $.session.cwd()
95 const ran = await next(e)
96 try {
97 await follow($, e.command, before)
98 } catch {}
99 return ran
100 })
101
102 on('classic.UserPromptSubmit', async ($, e, next) => {
103 const result = await next(e)
104 const isTyped = e.agent_id === undefined && (e.source === undefined || e.source === 'user')
105 if (!isTyped || e.session_title || result.sessionTitle || (await read($, titled))) return result
106 const prompt = e.prompt.trim()
107 const words = fallbackTitle(prompt)
108 if (prompt.startsWith('/') || SHORT_REPLY.test(prompt) || words.split(' ').length < 3) return result
109 await update($, titled, () => true)
110 return { ...result, sessionTitle: (await nameSession($, prompt)) ?? words }
111 })
112}
113types/index.d.ts 8 lines1export type RepoSpot = { dir: string; label: string; at: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'repo-context': { spot: RepoSpot | null; titled: boolean }
6 }
7}
8