Greets you with the shared wordmark above the prompt until the first message

git clone https://github.com/s1n7ax/nixos.git
nix flake update
sudo nixos-rebuild switch --upgrade --flake ./#desktop
#^^^^^^^^ profile name here
This configuration supports multiple profiles:
darwin-rebuild, not nixos-rebuild)# Build and switch to desktop profile
sudo nixos-rebuild switch --upgrade --flake ./#desktop
# Test desktop configuration without switching
sudo nixos-rebuild test --flake ./#desktop
# Build desktop configuration without switching
sudo nixos-rebuild build --flake ./#desktop
# Build and switch to server profile
sudo nixos-rebuild switch --upgrade --flake ./#server
# Test server configuration without switching
sudo nixos-rebuild test --flake ./#server
# Build server configuration without switching
sudo nixos-rebuild build --flake ./#server
This profile is managed by nix-darwin, so use darwin-rebuild instead of nixos-rebuild. The flake output is named macbook, not the machine's hostname — always select it with #macbook.
First-time bootstrap (before darwin-rebuild exists on the system):
# Run darwin-rebuild straight from the nix-darwin flake to install it.
# --extra-experimental-features is required if nix-command/flakes aren't
# already enabled in the stock macOS Nix install.
sudo nix run --extra-experimental-features 'nix-command flakes' \
nix-darwin/nix-darwin-26.05#darwin-rebuild -- switch --flake ~/nixos#macbook
After the first successful switch, darwin-rebuild is installed at /run/current-system/sw/bin. Subsequent rebuilds:
sudo strips the Nix directories from PATH via secure_path, so a plain sudo darwin-rebuild fails with command not found. Invoke it by full path:
# Build and switch to the macbook profile
sudo /run/current-system/sw/bin/darwin-rebuild switch --flake ~/nixos#macbook
# Build the macbook configuration without switching
sudo /run/current-system/sw/bin/darwin-rebuild build --flake ~/nixos#macbook
# Check what would change (dry run)
sudo /run/current-system/sw/bin/darwin-rebuild switch --flake ~/nixos#macbook --dry-run
Note: the
--flake ~/nixos#macbookargument is required. A baredarwin-rebuild switchdefaults to the flake at/etc/nix-darwinkeyed by hostname and will fail with a missing-darwinConfigurationsattribute error.
Dev tooling runs natively on macOS: the macbook profile imports the same home-manager tree as the Linux profiles and enables the feature set in profile/macbook/host-options.nix (neovim, fish, kitty, git/gh/acli, the language toolchains, CLI utilities). There is no linux-builder VM — Docker is provided by colima, started by home-manager when features.virtualization.docker is on.
Secrets are not available on macOS: the secrets flake input and the sops modules are imported only by profile/common, which the macbook profile does not use.
Convenience aliases (see system/home-manager/applications/alias.nix, Darwin half):
mac-update # cd ~/nixos && nix flake update
mac-rebuild # darwin-rebuild switch --flake ~/nixos#macbook
mac-clean # garbage-collect and optimise the store
# Apply home-manager changes only (faster for user-level changes)
home-manager switch --flake ./#desktop
# Update flakes to latest versions
nix flake update
# Check flake syntax and evaluate
nix flake check
# Show what would be built/downloaded (dry run)
nixos-rebuild switch --flake ./#desktop --dry-run
# Garbage collect old generations
sudo nix-collect-garbage -dhooks/register.tsx 109 lines1/**
2 * Claude Code start screen. Its logo can't be replaced, so the shared
3 * wordmark and greeting are drawn in the band above the prompt, until the
4 * first prompt, slash command or `!` command lands in the transcript
5 * (`prompt.submit` misses the last two).
6 *
7 * The hook sandbox has no Node and no local time zone, so the clock, host and
8 * branch come from `date`, `uname` and `git`, read once at session start.
9 *
10 * `banner.ts` and `identity.ts` are copied beside this file at build time
11 * (see `../../default.nix`).
12 */
13import { atom, read, update } from "claude-code";
14import type { EngineInterface, Register } from "claude-code";
15import type { Welcome } from "../types";
16import {
17 dateline,
18 gradients,
19 greeting,
20 mocha,
21 project,
22 shade,
23 signature,
24 wordmark,
25 wordmarkWidth,
26} from "./banner";
27
28const welcome = atom({ plugin: "welcome", key: "welcome" } as const, null);
29const isGreeted = atom({ plugin: "welcome", key: "isGreeted" } as const, false);
30
31const gutter = 3;
32const minimumTextWidth = 30;
33
34async function lookAround($: EngineInterface, cwd: string): Promise<Welcome> {
35 const [clock, host, branch, home] = await Promise.all([
36 $.process.run(["date", "+%Y %m %d %H %M"]),
37 $.process.run(["uname", "-n"]),
38 $.process.run(["git", "-C", cwd, "branch", "--show-current"]),
39 $.env.get("HOME"),
40 ]);
41 const [year = 1970, month = 1, day = 1, hour = 0, minute = 0] = clock.stdout.trim().split(" ").map(Number);
42 const branchName = branch.exitCode === 0 ? branch.stdout.trim() : "";
43 const place = project(cwd, home ?? undefined);
44
45 return {
46 hour,
47 date: dateline(new Date(year, month - 1, day, hour, minute)),
48 where: signature(branchName ? `${place} \ue0a0 ${branchName}` : place, host.stdout.trim()),
49 };
50}
51
52export const register: Register = (on) => {
53 on("session.start", async ($, e, next) => {
54 if (e.isInteractive) {
55 const found = await lookAround($, e.cwd);
56 await update($, welcome, () => found);
57 }
58
59 return next(e);
60 });
61
62 on("session.append", async ($, e, next) => {
63 const isFirstMove = e.agentId === undefined && (e.door === "prompt" || e.door === "command");
64 if (isFirstMove) await update($, isGreeted, () => true);
65
66 return next(e);
67 });
68
69 on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
70 const [state, greeted] = await Promise.all([read($, welcome), read($, isGreeted)]);
71
72 if (state === null || greeted || e.props.hasSurvey || e.props.view.agentId !== undefined) {
73 return next(e);
74 }
75
76 const { Box, Text } = $.ui.resolve(e);
77 const isWide = e.props.bodyColumns >= wordmarkWidth + gutter + minimumTextWidth;
78 const lines = [
79 <Text bold color={gradients.claude.at(-1)} wrap="truncate-end">
80 {greeting(state.hour)}
81 </Text>,
82 <Text color={mocha.subtext0} wrap="truncate-end">
83 {state.date}
84 </Text>,
85 <Text color={mocha.overlay1} wrap="truncate-end">
86 {state.where}
87 </Text>,
88 ];
89
90 return (
91 <Box flexDirection="column">
92 {wordmark.map((art, row) => (
93 <Box flexDirection="row">
94 {isWide && (
95 <Text>
96 {shade(art, gradients.claude).map(({ text, color }) => (
97 <Text color={color}>{text}</Text>
98 ))}
99 {" ".repeat(wordmarkWidth - [...art].length + gutter)}
100 </Text>
101 )}
102 {lines[row]}
103 </Box>
104 ))}
105 </Box>
106 );
107 });
108};
109types/index.d.ts 13 lines1/** What the band shows, read once at session start. */
2export type Welcome = {
3 hour: number;
4 date: string;
5 where: string;
6};
7
8declare module "claude-code" {
9 interface PluginState {
10 welcome: { welcome: Welcome | null; isGreeted: boolean };
11 }
12}
13