Saves GitHub Actions minutes: blocks gh run watch/rerun and gh pr checks --watch, and warns when a second PR is opened in the session.

Four small Claude Code mods. Two of them check the Bash commands Claude runs and block commits without a ticket and CI reruns. One tells Claude when a git worktree can be removed after it opens a pull request. One warns you when memory or disk is low.
![A terminal where commit-ticket blocks git commit -m "fix stuff" on the branch feature/ABC-12-login-redirect and allows "[ABC-12] Fix login redirect", and guard-actions blocks gh run rerun and allows gh run view.](docs/demo.gif)
The demo runs the real decision code of two mods with demo/run-hook.sh, so the commands are only checked and never run. Inside Claude Code the same code runs on every Bash command that Claude tries to run.
Claude Code loads every plugin in ~/.claude/skills/<name> in the next session. Clone this repository, then link the mods you want from its root:
for m in commit-ticket guard-actions close-worktree live-resources; do
ln -s "$PWD/$m" ~/.claude/skills/$m
done
To disable a mod, remove its link. To check a mod before you link it, run claude plugin validate commit-ticket and claude plugin test commit-ticket. The requirements are below.
You can write rules like "always put the ticket in the commit message" in CLAUDE.md, but Claude can forget them in a long session. commit-ticket and guard-actions check every Bash command before it runs, block the ones that break a rule, and tell Claude the reason and what to do instead. The logic of each mod is plain TypeScript in hooks/lib.ts, with unit tests next to it.
The rules are fixed in the code. There is no config file. If your ticket format or your CI rules are different, fork the repository and change hooks/lib.ts.
These are personal tools that I use every day. There are no releases and no stability promise, and the behavior can change.
claude plugin test --help works, your version has them. I use Claude Code 2.1.296.live-resources works only on macOS, because it reads memory with vm_stat.guard-actions and close-worktree are about the GitHub CLI (gh). commit-ticket and close-worktree need git.This mod reads the message of every git commit that Claude runs. When the branch name has a ticket, like feature/ABC-5-something, the message must start with a ticket in brackets, like [ABC-5] Fix overflow. If it does not, the mod blocks the commit and suggests the message with the ticket of the branch. It also blocks commits that only fix linting or formatting, like [ABC-5] Fix linting, because the result of an autofix (eslint --fix, prettier --write) should go inside the commit of the change that caused it.
The mod does not check branches without a ticket, merge commits, or messages that it cannot read with confidence, like -F file or --amend --no-edit. To skip the check once, add the comment # ok-commit at the end of the command.
This mod blocks gh run watch, gh run rerun and gh pr checks --watch, because every rerun or wait for CI uses GitHub Actions minutes, and many organizations pay for them. Read-only commands like gh run view or gh run list are allowed, also inside a sleep loop. When Claude opens a second pull request in the same session, the mod shows a message that suggests one integration pull request instead.
To run a blocked command once, add the comment # ok-actions at the end of the command.
After Claude runs gh pr create from a git worktree, this mod checks the worktree. If it has no uncommitted files and no unpushed commits, it tells Claude that the worktree can be removed and gives the exact git worktree remove command. If something is still pending, it tells Claude what is missing and asks it not to remove the worktree. The mod only gives advice. It never deletes anything.
Worktrees that nobody removes stay on disk, and many of them can fill it. The local and remote branches stay after the worktree is removed, so you can create it again with git worktree add if the review asks for changes.
In interactive sessions, this mod checks free memory and free disk space every 30 seconds. When memory goes under 2.5 GB or disk goes under 10 GB, it shows a status line and a short message, and the status line also counts the running test runners (jest, vitest, rspec, playwright, pytest and others). Under 1 GB of memory or 5 GB of disk, the status line is marked as critical. When everything is fine, it shows nothing.
It exists because test runners that run in parallel can use all the memory of a laptop and freeze it.
demo/run-hook.sh builds the Bash tool call that Claude Code would pass to a mod, pipes it as JSON into demo/run-hook.ts, and prints the decision. It supports commit-ticket and guard-actions, and it never runs the command:
demo/run-hook.sh guard-actions 'gh run watch 123'
To record the GIF again, install VHS and run vhs docs/demo.tape from the root of the repository. The tape works in a new temporary git repository.
The logic of each mod is in hooks/lib.ts, with its tests in hooks/lib.test.ts. The file hooks/register.ts only connects that logic to the hook events of Claude Code.
claude plugin validate <mod> # checks the manifest and the hooks
claude plugin test <mod> # runs the *.test.ts files of the mod
MIT. See LICENSE.
hooks/register.ts 30 lines1import type { Register } from 'claude-code'
2
3import { checkCommand } from './lib'
4
5export const register: Register = on => {
6 let prsCreated = 0
7
8 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
9 const verdict = checkCommand(e.command)
10
11 if ('deny' in verdict) {
12 return { deny: verdict.deny }
13 }
14
15 const ran = await next(e)
16
17 if (verdict.isPrCreate && ran.deny === undefined && ran.isError !== true) {
18 prsCreated += 1
19 if (prsCreated >= 2) {
20 $.ui.toast(
21 `You opened ${prsCreated} PRs in this session: each one runs CI. One integration PR is better.`,
22 { timeoutMs: 10_000 },
23 )
24 }
25 }
26
27 return ran
28 })
29}
30hooks/lib.ts 24 lines1// Explicit escape hatch: a command that has this comment passes without a check.
2export const OVERRIDE = '# ok-actions'
3
4export type Verdict = { deny: string } | { isPrCreate: boolean }
5
6const WATCH_OR_RERUN = /\bgh\s+run\s+(watch|rerun)\b/
7const CHECKS_WATCH = /\bgh\s+pr\s+checks\b[^\n|;&]*--watch\b/
8const PR_CREATE = /\bgh\s+pr\s+create\b/
9
10const WHY =
11 'Every rerun or wait for CI uses GitHub Actions minutes, and many organizations pay for them. ' +
12 'Run locally the same checks that CI runs (tests, linter, scanners), and push only once for each batch of fixes. ' +
13 `If you really need it, add ${OVERRIDE} at the end of the command.`
14
15export function checkCommand(command: string): Verdict {
16 if (command.includes(OVERRIDE)) return { isPrCreate: false }
17
18 if (WATCH_OR_RERUN.test(command) || CHECKS_WATCH.test(command)) {
19 return { deny: `guard-actions: blocked gh run watch/rerun and gh pr checks --watch. ${WHY}` }
20 }
21
22 return { isPrCreate: PR_CREATE.test(command) }
23}
24