Keeps the Mac from idle-sleeping while a turn or a background agent runs

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 71 lines1import type { EngineInterface, HookStream, ProcessSpawnChunk, ProcessSpawnResult, Register } from 'claude-code'
2
3const AGENT_TTL_MS = 3 * 60 * 60 * 1000
4const KEEPER = 'caffeinate -i -w $$ & i=0; while [ "$i" -lt 2880 ]; do echo .; sleep 15; i=$((i+1)); done'
5
6type Keeper = HookStream<ProcessSpawnChunk, ProcessSpawnResult>
7
8const awake = {
9 isTurnRunning: false,
10 agents: new Map<string, number>(),
11 keeper: undefined as Keeper | undefined,
12}
13
14const drain = async (started: Keeper) => {
15 try {
16 for await (const _ping of started) if (awake.keeper !== started) break
17 } catch {}
18 if (awake.keeper === started) awake.keeper = undefined
19}
20
21const hold = ($: EngineInterface) => {
22 if (awake.keeper !== undefined) return
23 const started = $.process.spawn({ argv: ['/bin/sh', '-c', KEEPER] })
24 awake.keeper = started
25 void drain(started)
26}
27
28const release = () => {
29 const held = awake.keeper
30 awake.keeper = undefined
31 held?.return({ code: null, signal: null }).catch(() => {})
32}
33
34const sync = async ($: EngineInterface) => {
35 const now = await $.clock.now()
36 for (const [id, since] of awake.agents) if (now - since > AGENT_TTL_MS) awake.agents.delete(id)
37 if (awake.isTurnRunning || awake.agents.size > 0) hold($)
38 else release()
39}
40
41export const register: Register = on => {
42 on('turn.start', async ($, e, next) => {
43 awake.isTurnRunning = true
44 await sync($)
45 return next(e)
46 })
47
48 on('turn.complete', async ($, e, next) => {
49 if (e.agentId === undefined) awake.isTurnRunning = false
50 else awake.agents.delete(e.agentId)
51 await sync($)
52 return next(e)
53 })
54
55 on('agent.spawn', async ($, e, next) => {
56 const spawned = await next(e)
57 if (e.background && spawned.agentId !== undefined) {
58 awake.agents.set(spawned.agentId, await $.clock.now())
59 await sync($)
60 }
61 return spawned
62 })
63
64 on('session.end', async ($, e, next) => {
65 awake.isTurnRunning = false
66 awake.agents.clear()
67 release()
68 return next(e)
69 })
70}
71