/profile: the account profile this session runs as

A declarative NixOS system and home configuration, focused on reproducibility and maintainability.
This repository contains my personal NixOS system configuration and home environment setup, managed through Nix flakes. It represents a transition from my previous Ansible-based setup (StanAngeloff/longitude) to a fully declarative system configuration.
The source tree is the source of truth. Rather than maintaining a parallel inventory here, look at:
home/apps/ — every user-space program I install, one directory per app.system/components/ — every system-level component (audio, networking, podman, …).home/gnome-shell/extensions/ — every GNOME Shell extension that's enabled.sudo is required by design)..chat file you own — plus Claude Code.<Super>t opens Ghostty, or focuses the existing window if one's already up. home/apps/ghostty/launch/<Super>s opens Voxize for voice input — see StanAngeloff/voxize. home/apps/voxize/<Ctrl><Shift>e opens unipicker in a Ghostty overlay window. The chosen codepoint lands in the clipboard via wl-copy. home/gnome-shell/keybindings.nix~/Pictures/Screenshots open in Swappy automatically (systemd user service + inotifywait). home/apps/swappy/launch/eog, a Swappy plugin adds an "annotate" action so you can mark up an image you're already viewing. home/apps/eog/less is replaced by ov everywhere, including git's pager. An ov-less.yaml keeps the less-style keybinds. home/apps/ov/git diff and git show use tig as the pager (with a perl ANSI stripper in front). home/apps/tig/prefix C-g toggles a tig status popup; once it's open, Alt+z toggles its size between 90% and 100% (outside the popup it falls back to resize-pane -Z). home/apps/tmux/abilities/tmux.tig.confd on a /nix/store/... path runs nix-diff /run/current-system <path> | ov in a popup. home/apps/tmux/abilities/tmux.nix-diff.confnsx <pkg> -- <cmd> is npx for nixpkgs — fetches <pkg> from the chosen channel and runs <cmd>. home/apps/nsx/preview <file.md> renders Markdown in Brave via headless nvim + the (revived) markdown-preview.nvim. The browser tab survives nvim's exit. home/apps/nixvim/plugins/markdown-preview-nvim/home/apps/zsh/default.nix.
├── home/ # Home Manager configuration
│ ├── apps/ # User-space applications (one directory per app)
│ ├── essentials/ # Core user environment, fonts, GTK, XDG
│ ├── gnome-shell/ # GNOME Shell extensions, settings, keybindings
│ └── services/ # User services (Bluetooth, GPG, SSH)
├── machines/ # Per-machine hardware configuration
├── modules/ # Custom Nix modules and shared options
├── pkgs/ # Local package overlays (e.g. Ghostty)
├── system/ # NixOS system configuration
│ ├── apps/ # System-wide applications
│ └── components/ # System components
├── contrib/ # Helper scripts (e.g. nixfmt-pretty.awk)
├── flake.nix # Flake inputs and outputs
├── Makefile # `make switch`, `make format`
└── treefmt.toml # Formatter wiring (nixfmt + shfmt + stylua + ruff/black)
*.ttf fonts for this): cd ~/.local/share/fonts
nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixos-unstable.tar.gz -p nerd-font-patcher -p parallel
parallel -j8 nerd-font-patcher --no-progressbars --careful --has-no-italic --single-width-glyphs --complete --name postscript {} ::: *.ttf
Be patient! The patching will take a while.
[!IMPORTANT] On a freshly cloned, not-yet-rebuilt system,
make switchwill not work — neithergnumakenornhis onPATHuntil the first rebuild has run home-manager activation. Use the stocknixos-rebuildinvocation for the very first switch, and only then move to the Makefile.
git clone https://github.com/StanAngeloff/nix-meridian.git
cd nix-meridian
sudo nixos-rebuild switch --flake .
That single rebuild brings up the NixOS system and the home-manager profile, which is what puts make, nh, and friends on PATH.
Once the initial rebuild has completed at least once, the Makefile is the front door:
make switch # nh os switch --ask .
make format # treefmt across nix/python/shell/lua
nh shows a diff and asks before applying, which beats raw nixos-rebuild for routine changes.
podman+.sudo cache disabled — timestamp_timeout=0 — with narrow NOPASSWD carve-outs for the nh/switch-to-configuration flow only.dropbox<pre>Dropbox is the easiest way to share and store your files online. Want to learn more? Head to https://www.dropbox.com/ In order to use Dropbox, you must download the proprietary daemon. Note: python3-gpg (python3-gpgme for Ubuntu 16.10 and lower) is not installed, we will not be able to verify binary signatures. [y/n] Traceback (most recent call last): File "[..]/urllib/request.py", line 1344, in do_open h.request(req.get_method(), req.selector, req.data, headers, File "[..]/http/client.py", line 1336, in request self._send_request(method, url, body, headers, encode_chunked) File "[..]/http/client.py", line 1382, in _send_request self.endheaders(body, encode_chunked=encode_chunked) File "[..]/http/client.py", line 1331, in endheaders self._send_output(message_body, encode_chunked=encode_chunked) File "[..]/http/client.py", line 1091, in _send_output self.send(msg) File "[..]/http/client.py", line 1035, in send self.connect() File "[..]/http/client.py", line 1470, in connect super().connect() File "[..]/http/client.py", line 1001, in connect self.sock = self._create_connection( ^^^^^^^^^^^^^^^^^^^^^^^^ File "[..]/socket.py", line 841, in create_connection for res in getaddrinfo(host, port, 0, SOCK_STREAM): ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "[..]/socket.py", line 976, in getaddrinfo for res in _socket.getaddrinfo(host, port, family, type, proto, flags): ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ socket.gaierror: [Errno -2] Name or service not known During handling of the above exception, another exception occurred: Traceback (most recent call last): File "[..]/bin/dropbox", line 568, in download for progress, status in download.copy_data(): ^^^^^^^^^^^^^^^^^^^^ File "[..]/bin/dropbox", line 200, in download_file_chunk with closing(opener.open(url)) as f: ^^^^^^^^^^^^^^^^ File "[..]/urllib/request.py", line 515, in open response = self._open(req, data) ^^^^^^^^^^^^^^^^^^^^^ File "[..]/urllib/request.py", line 532, in _open result = self._call_chain(self.handle_open, protocol, protocol + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "[..]/urllib/request.py", line 492, in _call_chain result = func(*args) ^^^^^^^^^^^ File "[..]/urllib/request.py", line 1392, in https_open return self.do_open(http.client.HTTPSConnection, req, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "[..]/urllib/request.py", line 1347, in do_open raise URLError(err) urllib.error.URLError: <urlopen error [Errno -2] Name or service not known> Error: Trouble connecting to Dropbox servers. Maybe your internet connection is down, or you need to set your http_proxy environment variable. Starting Dropbox...Traceback (most recent call last): File "[..]/bin/dropbox", line 1587, in <module> ret = main(sys.argv) ^^^^^^^^^^^^^^ File "[..]/bin/dropbox", line 1576, in main result = commands[argv[i]](argv[i+1:]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "[..]/bin/dropbox", line 1427, in start if not start_dropbox(): ^^^^^^^^^^^^^^^ File "[..]/bin/dropbox", line 763, in start_dropbox subprocess.Popen([DROPBOXD_PATH], preexec_fn=os.setsid, cwd=os.path.expanduser("~"), File "[..]/subprocess.py", line 1026, in __init__ self._execute_child(args, executable, preexec_fn, close_fds, File "[..]/subprocess.py", line 1955, in _execute_child raise child_exception_type(errno_num, err_msg, err_filename) FileNotFoundError: [Errno 2] No such file or directory: '/home/stan/.dropbox-hm'</pre>
This configuration draws inspiration from various sources and previous work:
MIT. Feel free to use any parts of this configuration as inspiration for your own setup.
hooks/register.ts 45 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// /profile: the account profile this session runs as, with that account's email and organization.
4// The bubble names the profile in CLAUDE_BUBBLE_PROFILE (bubble/modules/profiles/hooks.sh); the account comes from
5// .claude.json, which inside a bubble is the profile's own copy. A session outside the bubble runs as the default profile.
6
7type Account = { emailAddress?: string; organizationName?: string }
8
9async function readAccount($: EngineInterface): Promise<Account | undefined> {
10 const configPath = (await $.env.get('CLAUDE_CONFIG_DIR')) ?? `${await $.env.get('HOME')}/.claude`
11 try {
12 const config = JSON.parse(await $.fs.read(`${configPath}/.claude.json`)) as { oauthAccount?: Account }
13
14 return config.oauthAccount
15 } catch {
16 return undefined
17 }
18}
19
20export const register: Register = on => {
21 on('session.start', async ($, e, next) => {
22 await $.command.register({
23 name: 'profile',
24 description: 'Show the account profile this session runs as',
25 immediate: true,
26 })
27
28 return next(e)
29 })
30
31 on('command.run', { command: 'profile' }, async $ => {
32 const profile = (await $.env.get('CLAUDE_BUBBLE_PROFILE')) ?? 'the default profile (outside the bubble)'
33 const account = await readAccount($)
34 if (account?.emailAddress === undefined) {
35 return { text: `🪪 ${profile} · not logged in` }
36 }
37 // Claude.ai names a personal account's own organization after its email; that name repeats the email, so it is left out.
38 const organizationName =
39 account.organizationName === `${account.emailAddress}'s Organization` ? undefined : account.organizationName
40 const organization = organizationName ? ` (${organizationName})` : ''
41
42 return { text: `🪪 ${profile} · ${account.emailAddress}${organization}` }
43 })
44}
45