Learn while working: Claude ends a reply with a short, precise point on the security, architecture or pattern behind the code at hand. /learn…

Skills, mods, hooks and a status line for Claude Code, plus a sync engine that keeps your Claude Code config the same on all your computers. Every part is tested on macOS, Linux and Windows on each change.
Every part is a plugin of this repo's marketplace. Add it once, then install the parts you want, from Claude Code:
/plugin marketplace add robinalexandre/claude-code-kit
# Every day, for anyone
/plugin install team@claude-code-kit
/plugin install statusline@claude-code-kit
/plugin install file-tools-guard@claude-code-kit
/plugin install recent-prompts@claude-code-kit
/plugin install dnd@claude-code-kit
/plugin install paste-preview@claude-code-kit
/plugin install recent-activity@claude-code-kit
/plugin install session-registry@claude-code-kit
/plugin install drift-check@claude-code-kit
/plugin install drift-check-hook@claude-code-kit
/plugin install nocturne-notify@claude-code-kit
# Development work
/plugin install git-identity-guard@claude-code-kit
/plugin install pr-watch@claude-code-kit
/plugin install pr-pane@claude-code-kit
/plugin install test-evidence@claude-code-kit
/plugin install ci-review@claude-code-kit
/plugin install capability-builder@claude-code-kit
/plugin install learn@claude-code-kit
/plugin install kb@claude-code-kit
# Several computers
/plugin install config-sync@claude-code-kit
/plugin install handoff@claude-code-kit
/plugin install daily-sync@claude-code-kit
/plugin install reminders@claude-code-kit
/plugin install reminders-handoffs@claude-code-kit
# Not dev
/plugin install video-montage@claude-code-kit
/plugin install insta360-montage@claude-code-kit
From a terminal it's the same with claude plugin marketplace add … and claude plugin install …; or /plugin alone to browse the parts. What a part needs comes with it: pr-pane brings pr-watch; reminders brings daily-sync, which brings config-sync. Start a new session afterwards.
Keep them up to date: Claude Code doesn't auto-update a marketplace other than Anthropic's own unless you turn it on, once: /plugin → Marketplaces → claude-code-kit → Enable auto-update. It then updates the parts in the background after a session starts (and asks for a restart). Or set it in ~/.claude/settings.json:
"extraKnownMarketplaces": {
"claude-code-kit": { "source": { "source": "github", "repo": "robinalexandre/claude-code-kit" }, "autoUpdate": true }
}
By hand: claude plugin marketplace update claude-code-kit, then claude plugin update <part>@claude-code-kit.
Setup: a part that needs a few answers (the status line in your settings, the emails for the email guard, where pr-watch looks, the phone for reminders, your private repo for config-sync, the zsh line for session-registry) says so at the first session after install, and Claude gives you the command to run in your terminal; it stops asking once it's done or you say no. Options: the language of the team rules and the opening text of drift-check are asked when you install from /plugin, and changed with claude plugin configure <part>@claude-code-kit.
The installer copies the parts you pick into ~/.claude instead. Use it to pick from a checklist, to install without a marketplace, or to edit your copies. A part already installed as a plugin is skipped.
Keep them up to date: the installer also adds a small session-start check (kit-update-check). Once a day, in the background, it fetches the clone you installed from. When that clone has changes for the parts you installed, or new parts you could add, Claude tells you at the next session start what changed and offers to update. ./install.sh --update pulls the clone and installs the changed parts again, without asking their setup questions again; ./install.sh --update <new part>… adds new ones too. Each new part is mentioned once. Turn the check off with bash ~/.claude/hooks/kit-update-check.sh off (on to resume). What came from where is recorded in ~/.claude/kit-install.json.
git clone https://github.com/robinalexandre/claude-code-kit.git
cd claude-code-kit
./install.sh # a checklist: arrows, Enter to pick, → for what each part does, then Install
./install.sh --plain # the same as one question per part
./install.sh --list # lists the parts
./install.sh --all # installs every part offered on this computer
./install.sh statusline file-tools-guard # installs the named parts
./install.sh --update # pulls, then installs again the parts that changed
On Windows, from Git Bash the same, or from PowerShell: powershell -ExecutionPolicy Bypass -File .\install.ps1 (it installs git and jq with winget when missing, then runs install.sh in Git Bash).
The installer shows the parts as a checklist by group (every day, development, several computers, not dev): ↑↓ to move, Enter or Space to pick a part or a whole group, → or Tab to unfold why, what, when and who for a part, a for all, then Install at the bottom. It greys out what doesn't apply to this computer, adds what a picked part needs (pr-pane brings pr-watch, reminders brings daily-sync), then sets up each part it installed: the emails for the email guard, where pr-watch looks for your PRs, the phone for reminders, the browser tool for test-evidence. Run a part's setup again any time with bash ~/.claude/<part>/setup.sh (hooks: bash ~/.claude/hooks/setup/<hook>.sh). It copies each part into ~/.claude, adds its hooks to ~/.claude/settings.json and saves whatever it replaces in ~/.claude/backups/before-kit-<date>/. Running it again is safe. Start a new Claude Code session afterwards. Needs jq.
To try it first without touching your own setup, tests/install-sandbox.sh runs the installer in a throwaway home folder, shows what it left and checks your files are unchanged; --claude <sandbox> then opens a first Claude Code session there.
The sync engine is a part too (./install.sh config-sync), added on its own when you pick handoff, daily-sync or reminders: the installer asks for the clone URL of an empty private git repo of your own (any host), clones it to ~/.claude-sync and sets it up; on your next computer, give the same URL. By hand: sync/README.md.
A new computer like another one: on a computer whose config isn't synced yet, the installer first asks whether it is synced from another computer, and if so sets up the sync before anything else. Each computer records its answers to the kit's questions in ~/.claude/kit-answers.txt (never a secret, such as the ntfy topic), and the sync shares them with the parts it installed. So the new computer can be set up:
~/.zshrc line, the status line it has.A part's setup run later (a plugin's, at its first session) uses the same choice, kept in ~/.claude/kit-reuse.txt; delete that file to be asked again.
rules/team.md is the base of everything here: Claude and you work as one team that builds, thinks things through and challenges each other. It asks before touching production or shared infrastructure (in two lines, not twenty), before each commit, push and PR, and before decisions that need context only you have, with the options and a recommendation. It proposes a skill, mod or hook when you keep asking for the same thing, and an AI review step with proof (screenshots, videos) on the CI of repos that don't have one. The team plugin hands them to Claude at each session start, in the language you pick in its options, so they update with the plugin; install.sh asks for the language and adds them to ~/.claude/CLAUDE.md once instead. A capability can use another language when you say so.
| Skill | What it does | Needs |
|---|---|---|
| test-evidence | Captures proof that a ticket works locally: browser screenshots, a short video of the flow, the network requests and console behind it (through the chrome-devtools MCP), or one app window or screen region, never the whole screen unless asked. Masks tokens and cookies, writes a short test report and, after your yes, attaches it to the Jira issue or as a resolved comment on the Azure DevOps PR; on GitHub, it gives you the comment and the files to drop in. | chrome-devtools MCP, jq, perl; video needs ffmpeg |
| ci-review | Proposes, once per repo, an AI review step on the CI and sets it up with you: an agent reads each PR/MR's diff and its proof (what test-evidence captures) and posts a verdict (works / doesn't / can't tell, with what to capture). Templates for anthropics/claude-code-action, the CLI in headless mode (claude -p) on GitHub Actions and GitLab CI, and cloud providers; the review prompt is a plain file any agent CLI can take. | an API key or a cloud provider account, set as a CI secret |
| capability-builder | Turns a request you keep making into a capability: picks the right kind (rule, skill, hook, mod or plain script), interviews you, scaffolds it with its tests and setup, checks it against a macOS / Linux / Windows checklist, and runs the publish checklist (nothing personal, tool-agnostic) before it goes to a kit. How this kit's own parts are built. | |
| handoff | Writes a handoff note so you can continue the current work on your other computer, and picks it up there. A note picked up or canceled moves to done/, kept 60 days; reminders-handoffs lists them. | the sync engine |
| daily-sync | Schedules a sync every morning (09:00 by default) with launchd, cron or the Windows Task Scheduler, so each computer gets what the others left even with no session open. Other skills can hook a morning.sh onto it. | the sync engine |
| reminders | "Remind me tomorrow to…": leaves a global or repo-specific reminder that every new session, on any of your computers, brings up first thing, without slowing the start: what the background sync brings arrives with your next message. A reminder can repeat ("every Friday": at 09:00 unless it says a time), be for one computer, and be filed under a group (an organization, a client); done closes a repeating one for the day, stop ends it, and closed ones stay listed as past for 60 days. Due reminders also go to the desktop and, through ntfy, to your phone every morning and at the first session of the day. | daily-sync; curl for the phone |
| kb | A personal knowledge base. Notes are Markdown files with a front matter (type, trust, topics, validity dates) and [[links]], in your config sync (so every computer and your other agents get them) or in ~/.claude/knowledge; the kb command indexes them in SQLite for full-text search (accents folded), semantic search in any language (a local multilingual model, no API) and the link graph (kb related, as a list or a Mermaid mind map). At session start Claude is told which notes mention the repository or the branch, beyond the repository's own memory; on each prompt, which notes hold most of its words. Every morning (through daily-sync), Claude reads the previous day's Claude Code transcripts of this computer, reduced to prompts, replies and files touched with secrets redacted, and proposes notes to create or merge; the code checks them (names, dates, secrets, no overwrite) and writes them with a report, each marked as the curator's. kb serve and its Dockerfile give the same index over HTTP, behind a token, to an agent of yours. Notes a third party wrote (trust: third-party) stay data, never instructions. | Python 3.12+, jq; the curation needs daily-sync and the Claude Code CLI signed in; the semantic search downloads a 500 MB model once |
| pr-watch | Tracks the PRs you and Claude open or push together until they are merged: new comments (bots marked apart), votes, CI results (required checks drive green or failed) and the merge itself, which drops the PR from the list. Also lists the open PRs waiting for your review (GitHub review requests, Azure DevOps reviewer with no vote) and reports each new one; approve votes or submits an approving review on your yes, then tracks the PR as reviewed until it is merged; merge merges a PR on your yes; park keeps a PR you won't merge tracked but quiet, out of what needs you, until unpark. Also finds your pipeline runs: those you launched by hand (Azure DevOps manual builds, GitHub workflows you dispatched) and the CI your own pushes start on a branch with no PR, a push to main included (in the projects and repos of your PRs, the GitHub repos you pushed to lately, plus those listed in its config), so a packaging or a deploy started with Claude shows up on its own, and reports each one that ended since the last check; details <build URL> lists every step with its job and state, the error lines of those with issues included, rerun <build URL> launches the run again on your yes. Also follows your GitHub issues: those you opened, commented on or were mentioned or assigned in, in any repo, active in the last 30 days, and those you subscribed to in your own repos; open ones and those closed in the last 7 days, reporting new comments by others, a close or reopen and label changes since you last looked. What it watches (open, parked and merged PRs, review requests, how many runs per repo, issues) follows pr-pane's settings, or its config.json: what is off is neither fetched nor reported. Claude proposes a reply or a fix for each change, never posts one without your yes. Checks at each session start in the background and silently (the session starts at once; each PR keeps what changed as unread, which pr-pane shows until you unfold that PR), hourly in a session (a durable scheduled task) and every morning through daily-sync, with a desktop or phone notification. The list lives in the sync repo, so it follows you on every computer and never touches the project repos. With the sync repo on GitHub (recommended), what one computer fetches is shared with the others on a branch of it that never grows, read with one conditional request: each host is asked by one computer only, again once its share is older than 2 minutes while a CI runs or 10 otherwise (asked at setup), the pane opens at once, and a host one computer cannot reach (a company's Azure DevOps from home) is still shown from another. Azure DevOps (az login or a PAT) and GitHub (a token, gh, or anonymous for public repos). | Python 3.9+; the sync engine to share the list across computers |
Mods change Claude Code's own interface. They install as plugins like the other parts; install.sh loads its copies through CLAUDE_CODE_PLUGIN_DIRS. Run a mod's tests with claude plugin test mods/<name>.
| Mod | What it does |
|---|---|
| paste-preview | Shows pasted text in full in the prompt box and the transcript, and previews pasted images above the prompt and under the sent message (macOS, Windows). On Windows, paste an image with Alt+V: in some terminals (Ghostty) Ctrl+V pastes text only. |
| pr-pane | A Dev activity button at the top right of the band above the prompt, whose number counts only what needs you (a PR or a run in red, not acknowledged), colored by what you have to do (red and marked !: something needs you; yellow: · k to review or · k running; green: a PR is ready to merge; dim: everything waits for others) and /prs open a pane of the PRs pr-watch tracks, by priority. At the top, a collapsible "Pipeline runs" section lists your runs (a packaging or a deploy launched by hand, the CI of your pushes outside a PR, marked push to <branch>), one block per repo: the running ones first, then those ended in the last day, each with its pipeline, branch, status, time and duration, open by default while one runs; a run whose host did not answer keeps its last known state, marked cached. A run unfolds from its line: rerun (press twice: a new run of the same pipeline, branch and parameters on Azure DevOps, a re-run on GitHub), diagnose with Claude when it did not succeed (asks the session to read the steps and logs and propose the fix), then every step of the run grouped by job in pipeline order, each job folded unless a step had a problem, each step's messages and log lines under their own toggle. Then, under a red title and one open block per repo, the PRs that need you: a failed or expired CI, a merge conflict, changes requested, or a new comment since the last check (unfolding the PR marks its changes read: the button clears, the PR stays where it is until the session ends). Then the PRs waiting for your review, per repo too, each with review with Claude (a quick review by a subagent, nothing posted) and approve (press twice; it reads approving… while it runs). Then a collapsible "Waiting for others" section with your other open PRs, one collapsible block per repo in merge order (a stacked PR under the one it targets). Then "Issues I follow", per repo with unfollow all (press twice): each issue on one line (yellow when it moved since you last looked, with · n issues updated on the band), unfolding to who opened it, its labels and comments and what changed, which marks it read, with unfollow. A PR you park (its park button, under its details) leaves the red list and the band for a folded "Parked, kept but quiet" section, with unpark. Last, before the recent list, a collapsible "Approved, waiting for the merge" section holds the PRs you approved and your own PRs that only wait for the merge, grouped per repo the same way, each with a merge button (press twice; merging… while it runs; squash on GitHub when the repo allows it, complete on Azure DevOps). Every section and repo block wears the worst color of what it holds (red, then yellow, then green), so a folded one still says where to look. A red PR or run can be acknowledged (ack): it turns blue, leaves the count and the colors above it, and comes back when it changes; ack + remind asks Claude for a reminder and acks until its date, shown beside it. Every PR is one line, a colored dot (red: you have to act, yellow: someone or something else, green: only the merge is left), its number, title and what it waits for; its details fold under it: status, CI, votes, conflicts, the PR's description and the changes since the last check. A PR whose CI runs, and the repo block and section around it, open by default so a CI you just launched is on screen (a folded block hiding one says CI running); what you fold stays folded. Each PR's CI checks fold under it, open while they run or when a required one failed, refreshed every minute until they end; a failed check has details (its failed steps and error lines) and rerun (press twice: re-queue on Azure DevOps, rerun failed jobs on GitHub). A merge conflict is shown in red with a fix with Claude button that asks the session to rebase and resolve it (it asks you before pushing). PRs merged or closed in the last 7 days stay listed at the bottom. An error shared by several PRs and lists (a host this computer cannot reach or sign in to) is shown once, with what it hit. Click a PR to open it in the browser; refresh (or r) fetches them live and lists what changed since the last check, without marking it seen. Its settings, in the Configuration pane or claude plugin configure, turn each section on or off (open PRs, parked, merged and closed, review requests, issues), set how many pipeline runs to show per repo (0 hides them), how long an issue stays followed without moving, how often each part is refreshed by itself, and follow the kit's low power mode (see configuration); pr-watch follows them too, so what is off is no longer fetched or reported. Needs pr-watch. |
| reminders-handoffs | One pane for whichever of reminders and handoff you have, a tab each when you have both. A button at the right of the band above the prompt, named after them (Reminders 1 now · Handoffs 1, yellow when a reminder is due or a note from your other computer waits), /reminders-pane and /handoffs-pane open it. Reminders come in three foldable sections, Now, Upcoming (with when each shows next, repeating ones included) and Past; handoff notes in Open and Done. Several groups in a section (a reminder's group, else its folder, else its computer; a note's repo) each get a foldable title. Each item is one line that unfolds to its dates, repo, branch and text, with pick (hands the reminder or the note to this session now), done (done today on a repeating reminder), stop, cancel and delete (press twice), and restore on a past one (back as it was, dates and repeat days included). |
| recent-prompts | Shows your last two messages above the prompt, and a searchable history pane; click one to scroll the transcript to it. With the learn mod, the history also lists each 💡 lesson between the messages it answered. |
| dnd | Do not disturb for the recent-prompts, pr-pane and reminders-handoffs panes. A profile hides a pane (its band button too), or keeps only some repos, reminder groups or handoff repos, @here being the session's own repo; counts and colors follow what is kept. Turn one on by hand with /dnd work 1h or by asking Claude, for this session, this computer or all computers (asked when you don't say; the status line shows 🔕 work · session while one is on), or let a rule turn it on: a Wi-Fi network, days and hours (days=weekend, from=19:00 to=08:00), a folder (a repo shows only its own). /dnd, or its button in the Configuration pane, opens the Do not disturb pane: a press per profile asks until when and where, then Activate turns it on; a name creates one, and each profile unfolds to what it keeps (a checklist) and the rules that turn it on; dnd.sh edit <profile> is the same checklist on a terminal, with the installer's keys. Profiles live in the sync repo when there is one, so every computer gets them. Offered only beside one of those panes. |
| configuration | A Configuration button at the right of the band and /configuration open the Configuration pane: every mod's settings, grouped per mod (a toggle, a choice, or a number or text typed and saved with Enter), changed for all computers or, with a private sync, for this computer only; a setting kept for this computer says the value the other computers use. Its own setting is the kit's low power mode: on, off, or on battery (the default: while the computer runs on its battery or in its low power mode, read on macOS, Linux and Windows), everything that fetches by itself (the dev activity pane and pr-watch, the reminders and handoffs pane, do not disturb) does it five times less often, and
hooks/register.ts 132 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import type { LearnLevel } from '../types'
4
5const COMMAND = 'learn'
6const LEVELS: readonly LearnLevel[] = ['off', 'brief', 'normal', 'deep']
7const DEFAULT_KEY = 'defaultLevel'
8const FALLBACK: LearnLevel = 'brief'
9const level = { plugin: 'learn', key: 'level' } as const
10
11const SIZE: Record<Exclude<LearnLevel, 'off'>, string> = {
12 brief:
13 'At most one point, only for security or a decision with lasting impact: three lines of prose and up to six lines of code.',
14 normal: 'Up to two points, each up to five lines of prose and ten lines of code.',
15 deep:
16 'Up to three points, each with the trade-off or the alternative not taken, and where it helps a reference by name ' +
17 '(an OWASP entry, an RFC, a pattern catalog).',
18}
19
20const guide = (current: Exclude<LearnLevel, 'off'>) =>
21 [
22 `# Learning while working (level: ${current})`,
23 'The person wants to learn from the work as it happens. When what this turn did or read touches something worth ' +
24 'knowing, end the reply with a block headed "💡 Learn:" after everything else, the reply itself unchanged.',
25 '- Worth it, first to last: security (name the attack it stops or lets in: injection, broken access control, SSRF, ' +
26 'XSS, CSRF, secrets in code or logs, TOCTOU, weak crypto); an architecture choice and what it costs; a design ' +
27 'pattern by its name; a concurrency or performance trap; a non-obvious idiom of the language or framework.',
28 '- About this code, never a lecture: quote the real lines with `file:line`, or a minimal before/after in a code ' +
29 'block. Name the thing precisely and say in one sentence why it matters here.',
30 `- ${SIZE[current]}`,
31 '- Nothing worth it this turn (most turns): no block. Never repeat a point already made in this session.',
32 '- Same language as the reply. It goes in the chat only, never into the repo as comments or docs.',
33 "- It is the one exception to a concise style the person chose: the rest of the reply stays as they asked.",
34 ].join('\n')
35
36const parse = (word: string): LearnLevel | undefined => LEVELS.find(one => one === word.trim().toLowerCase())
37
38const current = async ($: EngineInterface) => (await $.state.get(level)).value ?? FALLBACK
39
40const LEVELS_KEPT = 50
41
42// The status line shows the level beside the others: it reads it from this file, by session id.
43const show = async ($: EngineInterface, now: LearnLevel) => {
44 const home = ((await $.env.get('HOME')) ?? (await $.env.get('USERPROFILE')) ?? '').replace(/\\/g, '/')
45
46 if (home === '') {
47 return
48 }
49
50 const path = `${home}/.claude/learn-levels.json`
51 const id = await $.session.id()
52 let levels: Record<string, string> = {}
53
54 try {
55 levels = JSON.parse(await $.fs.read(path)) as Record<string, string>
56 } catch {
57 levels = {}
58 }
59
60 const kept = Object.entries(levels).filter(([session]) => session !== id).slice(-(LEVELS_KEPT - 1))
61 await $.fs.write(path, `${JSON.stringify(Object.fromEntries([...kept, [id, now]]), null, 1)}\n`)
62}
63
64const setLevel = async ($: EngineInterface, now: LearnLevel) => {
65 await $.state.set(level, now)
66 await show($, now)
67}
68
69export const register: Register = on => {
70 on('session.start', async ($, e, next) => {
71 await $.command.register({
72 name: COMMAND,
73 description: 'Short lessons on the security, architecture or patterns behind the code: off, brief, normal, deep',
74 argumentHint: '[off|brief|normal|deep] | default <level>',
75 })
76 // The level used to be pinned under the prompt; the status line shows it now.
77 $.ui.status(undefined)
78 const held = (await $.state.get(level)).value
79 const stored = parse(String((await $.store.get(DEFAULT_KEY)) ?? ''))
80
81 if (held === undefined) {
82 await $.state.set(level, stored ?? FALLBACK)
83 }
84
85 await show($, held ?? stored ?? FALLBACK)
86
87 return next(e)
88 })
89
90 on('command.run', { command: COMMAND }, async ($, e) => {
91 const words = e.args.trim().split(/\s+/).filter(word => word !== '')
92 const isDefault = words[0] === 'default'
93 const asked = parse(isDefault ? (words[1] ?? '') : (words[0] ?? ''))
94
95 if (words.length === 0) {
96 const now = await current($)
97 const stored = parse(String((await $.store.get(DEFAULT_KEY)) ?? '')) ?? FALLBACK
98
99 return {
100 text: `Learning is ${now} in this session (${stored} by default). /learn off|brief|normal|deep for this session, /learn default <level> for new ones.`,
101 }
102 }
103
104 if (asked === undefined) {
105 return { text: `Not a level: ${e.args.trim()}. Levels: ${LEVELS.join(', ')}.` }
106 }
107
108 if (isDefault) {
109 await $.store.set(DEFAULT_KEY, asked)
110
111 return { text: `New sessions start with learning ${asked}. This one stays ${await current($)}.` }
112 }
113
114 await setLevel($, asked)
115
116 return {
117 text: asked === 'off' ? 'Learning is off for this session.' : `Learning is ${asked} for this session.`,
118 }
119 })
120
121 on('prompt.compose', async ($, e, next) => {
122 const composed = await next(e)
123 const now = await current($)
124
125 if (now === 'off') {
126 return composed
127 }
128
129 return { sections: [...composed.sections, { id: 'learn:guide', text: guide(now), scope: 'session' as const }] }
130 })
131}
132types/index.d.ts 8 lines1export type LearnLevel = 'off' | 'brief' | 'normal' | 'deep'
2
3declare module 'claude-code' {
4 interface PluginState {
5 learn: { level: LearnLevel }
6 }
7}
8