Default bundle: global instructions, the Night City theme and mod, desktop notifications, Context7, plus core plugins from claude-plugins-official as…

The default bundle, on every machine:
rules.md, added to the context at session start..mcp.json): up-to-date library docs.It no longer depends on superpowers, ponytail or code-review. superpowers and ponytail are skill packs inside the flows plugin that only its conductors read, so outside them Claude runs without ponytail's rules and band and without superpowers' session bootstrap. The installer removes old installs of both.
Night City, Context7 and the notifications were plugins of their own (night-city, mcp-basic, notify) until 0.3.0; the installer removes those and keeps Night City picked in /theme.
<img src="assets/mascot-512.png" width="192" align="right" alt="Onibi: a hooded netrunner in a respirator, her chrome cybereye a glowing eight-point spark">
A toned-down synthwave Claude Code theme, "Night City": a dusk-indigo background, lavender text, dusty-pink Claude, peach inline code and permission prompts, soft teal suggestions and bash, muted violet instead of grey for dim text. It is the house look: tui-review's pane, the notifications and the installer's screens use the same palette. Pick it in /theme (Claude Code does not let a plugin select a custom theme).
Beyond the theme:
printf '\033]111\007'.▚▞ FLATLINE ▞▚ once per turn on a failing tool, ◢◤ JACKED OUT ◢◤ when a main turn ends.€$1.23) and a ナイトシティ sign fading pink to teal. It needs a Nerd Font in the terminal. The installer writes it with the ccstatusline toggle and replaces the stock config earlier versions wrote.syntaxHighlightingDisabled) and sets CLAUDE_CODE_TMUX_TRUECOLOR=1: inside tmux Claude Code otherwise draws in 256 colours and the theme's indigos turn grey.Syntax highlighting: Claude Code colours code and diffs with a fixed Monokai palette for every dark theme, which a theme can't change, and it ignores the theme's diff colours while highlighting is on (anthropics/claude-code#57036). With it off (ctrl+t in /theme), diffs use Night City's mint and coral and code takes the theme's text colour.
The palette starts from the Cyberpunk Neon theme of ccthemes (MIT) and is retuned to softer synthwave colours; see THIRD_PARTY.md in the repository root.
Onibi (鬼火, "demon fire": the ghost lights of Japanese folklore that drift through the dark and lead people to their end). The netrunner above is the theme's mascot, an original 64×64 pixel-art character in the theme's palette: a respirator with an LED bar, an undercut with a circuit tattoo, and a chrome plate whose cybereye is an eight-point spark, a nod to Claude rather than Anthropic's logo. assets/mascot.png is the 1x original, assets/mascot-512.png the 8x nearest-neighbour scale; she is also the notification icon. All are drawn by scripts/draw-mascot.mjs in the repository root; edit the drawing there and rerun it rather than editing the PNGs.
Checked with relative luminance; all are at least 4.5:1. background is an undocumented token that the theme sets anyway.
| Foreground | on background | on user bar | on bash bar |
|---|---|---|---|
text #E6DCF7 | 13.44 | 11.29 | 11.15 |
permission #F5B97A | 10.20 | 8.57 | 8.46 |
error #F2788C | 6.61 | 5.55 | 5.48 |
inactive #9A90C2 | 6.02 | 5.06 | 5.00 |
text on the diff backgrounds: added 9.80, removed 10.56, added word 5.94, removed word 7.03, added dimmed 11.25, removed dimmed 12.22.
A desktop notification when Claude finishes a turn or waits for you, so you can work in another window (or on another monitor) and come back when it's your move.
It fires on:
Stop): a Night City line ("Come look at your Agent") and the first line of Claude's answer;Notification): a Night City line ("Songbird requests your help") and Claude Code's message;PreToolUse on AskUserQuestion, which raises no notification of its own): a waiting line and the question.The popup comes from Onibi, Night City's pixel-art netrunner: her face is the icon (icon.png, a brightened crop written by scripts/draw-mascot.mjs; never edit it by hand) and, on Linux, she is the sender. The title is the project folder, then the status in bold and Claude's line below it. Windows shows "Windows PowerShell" as the sender, since the toast borrows PowerShell's app id.
It stays quiet:
set -g focus-events on in your tmux config; without it tmux never knows about focus and every event pops up;claude -p, the Agent SDK, CI).Outside tmux on Linux it always notifies. macOS gets no notifications.
| Platform | How | Needs |
|---|---|---|
| Linux | busctl --user to org.freedesktop.Notifications, falling back to notify-send | systemd's busctl or libnotify's notify-send; a notification daemon |
| Windows | a WinRT toast from Windows PowerShell, under PowerShell's app id | Windows PowerShell 5.1 (built in); notifications on for it in Settings |
Talking to the notification daemon directly means tmux needs no allow-passthrough. The hook runs in the background (async), so a slow PowerShell start doesn't hold up the turn. Claude's text reaches the notifier as an argument or an environment variable, never as script source.
The installer sets "preferredNotifChannel": "notifications_disabled" in ~/.claude/settings.json, so Claude Code's own notification doesn't double up. Installed by hand, set it yourself.
hooks/register.tsx 86 lines1// Flavor: Night City spinner and turn-line words, status-line reactions, the terminal background.
2import type { EngineInterface as $, Register, Timer } from 'claude-code';
3
4import { RESET, SET, isNightCity, paint } from './background';
5import { SPINNER_WORDS, TURN_WORDS, pick } from './words';
6
7export const GLITCH_MS = 400;
8export const FLARE_MS = 600;
9// Reactions go in the plugin's own status line, which sits beside the user's statusLine command, not over it. Plain
10// text: the status line takes no colors without escape sequences, which a mod doesn't write.
11const GLITCH = '▚▞ FLATLINE ▞▚';
12const FLARE = '◢◤ JACKED OUT ◢◤';
13
14let glitched = false; // one glitch per main turn
15let reaction: Timer | undefined;
16
17function react($: $, text: string, ms: number) {
18 reaction?.cancel();
19 $.ui.status(text);
20 const mine = $.clock.after(ms, () => {
21 if (reaction === mine) $.ui.status(undefined);
22 });
23 reaction = mine;
24}
25
26// The terminal background follows the theme: Night City selected paints it, anything else resets it.
27let isTerminal = false;
28let isPainted = false;
29
30async function background($: $, on: boolean) {
31 if (on !== isPainted && (await paint($, on ? SET : RESET))) isPainted = on;
32}
33
34export const register: Register = (on) => {
35 glitched = false;
36 reaction = undefined;
37 isTerminal = false;
38 isPainted = false;
39
40 on('turn.start', (_$, e, next) => {
41 glitched = false;
42 return next(e);
43 });
44 on('tool.call', async ($, e, next) => {
45 const ran = await next(e);
46 if (ran.isError === true && !glitched && e.agentId === undefined) {
47 glitched = true;
48 react($, GLITCH, GLITCH_MS);
49 }
50 return ran;
51 }).catch((_$, e, next) => next(e));
52 on('turn.complete', ($, e, next) => {
53 if (e.agentId === undefined) react($, FLARE, FLARE_MS);
54 return next(e);
55 });
56
57 on('ui.render', { component: 'Spinner' }, (_$, e, next) =>
58 next({ ...e, props: { ...e.props, word: pick(SPINNER_WORDS, e.props.word) } }),
59 );
60 on('ui.render', { component: 'TurnDuration' }, (_$, e, next) =>
61 next({ ...e, props: { ...e.props, word: pick(TURN_WORDS, e.props.word) } }),
62 );
63
64 on('session.start', async ($, e, next) => {
65 const started = await next(e);
66 isTerminal = e.isInteractive && e.surface === 'terminal';
67 if (isTerminal) {
68 void $.config.list().then(
69 (rows) => background($, isNightCity(rows.find((row) => row.key === 'theme')?.value)),
70 () => {},
71 );
72 }
73 return started;
74 });
75 on('config.set', { key: 'theme' }, async ($, e, next) => {
76 const set = await next(e);
77 if (isTerminal && set.deny === undefined) void background($, isNightCity(set.value));
78 return set;
79 }).catch((_$, e, next) => next(e));
80 // After a /clear or a resume the process, and the terminal, go on.
81 on('session.end', async ($, e, next) => {
82 if (isPainted && e.reason !== 'clear' && e.reason !== 'resume') await background($, false);
83 return next(e);
84 });
85};
86hooks/background.ts 60 lines1// The terminal's own background: Claude Code leaves empty cells to the terminal, which no theme token reaches, so this
2// sends OSC 11 (set the background) and OSC 111 (reset it) to the terminal Claude runs in. A mod can't write to the
3// terminal itself, so a child process finds it and writes there.
4import type { EngineInterface as $ } from 'claude-code';
5
6// The theme's `background` token, so the empty cells match the painted ones.
7export const SET = ']11;#1A1626';
8export const RESET = ']111';
9
10// The child has no tty of its own: walk up to the first ancestor with one (Claude's) and write there.
11export const SH = `p=$PPID
12while [ "$p" -gt 1 ]; do
13 t=$(ps -o tty= -p "$p" | tr -d ' ')
14 case "$t" in ''|'?'|'??') p=$(ps -o ppid= -p "$p" | tr -d ' ') ;; *) printf '\\033%s\\007' "$1" > "/dev/$t"; exit 0 ;; esac
15done
16exit 1`;
17
18// Windows: no /dev/pts. The engine starts the child hidden (windowsHide), which gives it a new console of its own, so
19// it swaps that for its parent's (Claude's) before opening CONOUT$, and lets go again so its own exit leaves Claude's
20// console alone. CONOUT$ is opened through CreateFileW because .NET's FileStream won't open device names.
21export const PS = `$sig = '[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] public static extern IntPtr CreateFileW(string n, uint a, uint s, IntPtr p, uint c, uint f, IntPtr t); [DllImport("kernel32.dll")] public static extern bool FreeConsole(); [DllImport("kernel32.dll")] public static extern bool AttachConsole(int p);'
22$k = Add-Type -MemberDefinition $sig -Name K -Namespace NightBg -PassThru
23$null = $k::FreeConsole()
24if (-not $k::AttachConsole(-1)) { exit 1 }
25$h = $k::CreateFileW('CONOUT$', 0x40000000, 3, [IntPtr]::Zero, 3, 0, [IntPtr]::Zero)
26if ($h -eq [IntPtr](-1)) { $null = $k::FreeConsole(); exit 1 }
27$fs = New-Object IO.FileStream((New-Object Microsoft.Win32.SafeHandles.SafeFileHandle($h, $true)), [IO.FileAccess]::Write)
28$b = [Text.Encoding]::ASCII.GetBytes([string][char]27 + $env:NIGHT_CITY_OSC + [char]7)
29$fs.Write($b, 0, $b.Length); $fs.Close(); $null = $k::FreeConsole()`;
30// Passed as -EncodedCommand (base64 of UTF-16LE; PS is ASCII), not on stdin: once FreeConsole drops the console,
31// PowerShell stops reading stdin and exits 0 without running the lines after it.
32export const PS_ENCODED = btoa([...PS].map((c) => c + '\0').join(''));
33
34// Absolute paths: a bare name is looked up in the session's working directory first on Windows, so a project could
35// plant its own `sh.exe` or `powershell.exe`.
36// shortcut: assumes Windows lives in C:\Windows; read %SystemRoot% if someone installs it elsewhere.
37export const SH_PATH = '/bin/sh';
38export const PS_PATH = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
39
40// The plugin theme's stored value is `custom:<plugin>:night-city`, so `custom:dead-skills:night-city` (undocumented, read from the binary).
41export const isNightCity = (theme: unknown) => typeof theme === 'string' && theme.endsWith(':night-city');
42
43// Mods have no platform API: try the POSIX route, fall back to PowerShell when sh is missing or finds no tty.
44// shortcut: a crash or kill skips the reset and the terminal keeps the color until `printf '\033]111\007'`.
45export async function paint($: $, osc: string): Promise<boolean> {
46 try {
47 if ((await $.process.run([SH_PATH, '-c', SH, 'sh', osc])).exitCode === 0) return true;
48 } catch {}
49 try {
50 const r = await $.process.run([PS_PATH, '-NoProfile', '-NonInteractive', '-EncodedCommand', PS_ENCODED], {
51 env: { NIGHT_CITY_OSC: osc },
52 });
53 if (r.exitCode === 0) return true;
54 $.ui.log(`night-city: background not set: ${r.stderr.trim().slice(0, 200)}`, { to: 'debug' });
55 } catch (err) {
56 $.ui.log(`night-city: background not set: ${err}`, { to: 'debug' });
57 }
58 return false;
59}
60hooks/words.ts 42 lines1export const SPINNER_WORDS: readonly string[] = [
2 'Breaching ICE',
3 'Jacking in',
4 'Running the net',
5 'Spoofing the Blackwall',
6 'Slicing the firewall',
7 'Dialing the fixer',
8 'Scanning the braindance',
9 'Overclocking the deck',
10 'Pinging the netrunner',
11 'Hacking the mainframe',
12 'Rerouting through the grid',
13 'Splicing the datastream',
14 'Cracking the cipher',
15 'Riding the neon',
16];
17
18// Past tense, so each reads before "for 3s".
19export const TURN_WORDS: readonly string[] = [
20 'Jacked out',
21 'Flatlined the bug',
22 'Ghosted the ICE',
23 'Breached the Blackwall',
24 'Ran the net',
25 'Cleaned the trace',
26 'Fried the firewall',
27 'Cashed the eddies',
28 'Slipped past the corpos',
29 'Wired the deck',
30 'Sliced through',
31 'Zeroed the daemon',
32];
33
34// FNV-1a over the seed, so one seed always lands on one word.
35export function hash(seed: string): number {
36 let h = 0x811c9dc5;
37 for (const c of seed) h = Math.imul(h ^ c.codePointAt(0)!, 0x01000193) >>> 0;
38 return h;
39}
40
41export const pick = (words: readonly string[], seed: string) => words[hash(seed) % words.length]!;
42