Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 46 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ If you find a bug, please open a GitHub Issue using the bug report template.
Running the switch builds:

- System settings (dark mode, key repeat, dock, Finder, trackpad)
- Homebrew apps (casks and CLI tools)
- Homebrew apps (casks and CLI tools, including VS Code Insiders)
- Nix user packages (ripgrep, fd, fzf, jq, lazygit, Neovim, Hack Nerd Font)
- Shell (zsh, aliases, starship prompt)
- Editor (Neovim config with the rose-pine moon theme)
- Terminal (WezTerm config with the rose-pine moon theme and dimmed unfocused windows)
- Agent configs (Claude, Codex, opencode all share one AGENTS.md)
- Optional Pi theme and local extensions, generic UI settings and model overrides, plus two deliberately pinned third-party Pi packages
- SSH server (Remote Login), key-only auth, for connecting from another machine on the LAN (e.g. VS Code Remote-SSH)
- Git identity, GitHub SSH auth, and commit signing, using a dedicated machine key pulled non-interactively from 1Password via a Service Account (no human required - this box runs unattended)

## Prerequisites

Expand Down Expand Up @@ -91,19 +93,7 @@ If you clone it, review these before you run `bootstrap.sh`:
All three have to match.
- **CPU architecture**, `hostPlatform` in `configuration.nix` (see Prerequisites above).

**Git identity:** this config deliberately does not set your git name or email.
Git will stop your first commit and tell you to set them (`git config --global user.name "Your Name"` and `git config --global user.email you@example.com`).
If you'd rather manage that declaratively, add this back to `home.nix` with your own identity:

```nix
programs.git = {
enable = true;
settings.user = {
name = "Your Name";
email = "you@example.com";
};
};
```
**Git identity:** `user.name`/`email` aren't in this repo at all - they come from a `GIT_NAME`/`GIT_EMAIL` item in 1Password, materialized by `rebuild.sh` straight to `~/.config/git/config-local` (outside this repo, same as the SSH keys) and pulled in via `programs.git.includes`, so this public repo never hardcodes anyone's real name or email. `user.signingkey` points at `~/.ssh/id_ed25519_mac_signing`, which likewise doesn't exist until "GitHub SSH authentication & commit signing" below is done. If you clone this repo, add your own `GIT_NAME`/`GIT_EMAIL` item instead (see that section for the exact item shape), or just hardcode `programs.git.settings.user` in `home.nix` if you don't want the 1Password indirection.

**Homebrew cleanup warning:** `configuration.nix` sets `homebrew.onActivation.cleanup = "zap"`.
That means every time you switch, Homebrew removes any package or cask on your machine that isn't listed in the `brews` and `casks` arrays in `configuration.nix`.
Expand Down Expand Up @@ -172,6 +162,48 @@ Both packages execute with your full user permissions and must be trusted like a

Home Manager deliberately does not manage `~/.pi/agent` itself, or Pi authentication, sessions, trust decisions, caches, npm/git package trees, or any other runtime state. The model overrides contain no credentials or endpoint settings, do not choose a default model, and only take effect after you authenticate Pi yourself. This remains an additive post-video layer: it does not install Pi, a launcher, or package source code into this repository.

## Remote access (SSH / VS Code Remote-SSH)

`configuration.nix` declares `services.openssh.enable = true;`, which turns on macOS's built-in Remote Login (the same sshd behind System Settings > Sharing) on every switch, and disables password authentication (`PasswordAuthentication no`, `KbdInteractiveAuthentication no`) so only key-based logins are accepted.

`home/.ssh/authorized_keys` is symlinked to `~/.ssh/authorized_keys` and ships with a placeholder line. To let another machine (e.g. a Windows PC running VS Code Insiders' Remote-SSH extension) connect:

1. On that machine, generate a keypair if you don't already have one: `ssh-keygen -t ed25519`.
2. Replace the placeholder line in `home/.ssh/authorized_keys` with the contents of the resulting `.pub` file.
3. Run `./rebuild.sh` on the Mac.

This setup assumes both machines are on the same local network - it doesn't open any port on your router or configure a tunnel. For access from outside your LAN, put something like Tailscale in front of it rather than port-forwarding SSH directly to the internet.

`visual-studio-code@insiders` is in the `casks` list, giving this Mac its own local VS Code Insiders install. That's separate from the Remote-SSH connection itself: when you connect from the Windows PC's VS Code Insiders, the Remote-SSH extension downloads and runs its own remote server component on the Mac automatically over the SSH connection the first time you connect - no separate install step for that part, since macOS already ships the `curl`/`tar` it needs.

## GitHub SSH authentication & commit signing (1Password)

This Mac runs unattended - nobody is sitting at it to approve a Touch ID prompt, and VNC-ing in every time git wants to push or sign a commit isn't "hands off." So this isn't 1Password's interactive SSH agent (that always requires a human to approve each use, every time it locks or restarts - fine for a laptop, not for a server). Instead:

- A dedicated, non-default 1Password vault (`mac-automation`) holds two SSH keys used **only** by this machine: `dotfiles-mac-auth` and `dotfiles-mac-signing`, kept separate so a problem with one never touches the other.
- A **Service Account** scoped read-only to just that vault authenticates non-interactively - no vault unlock, no biometrics, no human required.
- `home/.ssh/*.tmpl` are committed templates containing only `op://` references (safe - no secrets). `rebuild.sh` runs `op inject` after every `darwin-rebuild switch` to materialize the real private keys, public keys, and `allowed_signers` straight into `~/.ssh/`, entirely outside both this git repo and the Nix store (which is world-readable, so secrets must never pass through it).
- `home/.ssh/config` routes `github.com` at the local materialized key directly (`IdentityAgent none`), and falls back to 1Password's interactive agent for every other host - so a human still gets the vault-gated, private-key-never-touches-disk experience for their own ad hoc SSH use.
- Git signing uses git's default `ssh-keygen`-based signer against the local key file - no `op-ssh-sign`, no 1Password dependency at commit time.
- The same vault also holds a `dotfiles-personal` item with `GIT_NAME`/`GIT_EMAIL` fields, injected into `~/.config/git/config-local` and pulled in via `programs.git.includes` - so this repo's `home.nix` never hardcodes anyone's real name or email either.

**The trade-off, stated plainly:** the private keys now exist as ordinary files on this machine's disk, protected by Unix permissions and FileVault-at-rest - not "held only inside 1Password's vault." That's the same security posture as any standard CI/deploy key, not stronger. It's the accepted trade-off for unattended automation; it is a real downgrade from the interactive-agent model, not a wash.

One-time setup:

1. In 1Password, create the `mac-automation` vault (Service Accounts can't be granted access to your Personal/Private/Shared vault, so it has to be a fresh one).
2. In that vault, create `dotfiles-mac-auth` and `dotfiles-mac-signing` as SSH Key items (+ New Item > SSH Key > Generate, type ed25519), and a `dotfiles-personal` item (any item type with custom text fields) with `GIT_NAME` and `GIT_EMAIL` fields set to your actual name and email.
3. Create a Service Account, read-only, scoped to only the `mac-automation` vault. Copy its token immediately - 1Password shows it exactly once.
4. On this machine: `mkdir -p ~/.config/op && chmod 700 ~/.config/op`, save the token to `~/.config/op/service-account-token`, then `chmod 600` it. That path is outside `~/.dotfiles`, so it can never end up in this repo.
5. Run `./rebuild.sh`. It installs `1password-cli` (the `op` binary, via the `casks` list), then injects the keys and git identity into `~/.ssh/` and `~/.config/git/config-local`.
6. On GitHub, go to Settings > SSH and GPG keys > New SSH key. Add `~/.ssh/id_ed25519_mac_auth.pub`'s contents with key type "Authentication Key", and `~/.ssh/id_ed25519_mac_signing.pub`'s with key type "Signing Key".
7. Point this clone at GitHub over SSH: `git remote set-url origin git@github.com:<you>/<repo>.git`.
8. Verify: `ssh -T git@github.com` should greet you by username with no prompt at all, and `git commit --allow-empty -m test && git log --show-signature -1` should show a good SSH signature. GitHub also shows a "Verified" badge on pushed commits signed this way.

Rotating a key later is just: generate a new one in the 1Password item, re-run `./rebuild.sh` (`op inject --force` overwrites the local file), and update the GitHub-registered public key to match.

If you're setting this up on a laptop you actually sit at instead of a headless box, skip all of this and just use 1Password's interactive SSH agent directly - point `home/.ssh/config`'s `IdentityAgent` at 1Password's socket for all hosts, point `git`'s `gpg.ssh.program` at `op-ssh-sign`, and accept the occasional Touch ID prompt in exchange for the private key never touching disk at all. That's a better trade for a machine a person is actually present at.

## Notes

The first time you launch `nvim`, it bootstraps [lazy.nvim](https://github.com/folke/lazy.nvim) by cloning plugins from GitHub.
Expand Down
13 changes: 13 additions & 0 deletions configuration.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
home = "/Users/${user}";
};
system.stateVersion = 6;
# Remote Login (Apple's built-in sshd), for VS Code Remote-SSH from another
# machine on the LAN. Key-only: see home/.ssh/authorized_keys.
services.openssh = {
enable = true;
extraConfig = ''
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 50
'';
};
system.defaults = {
NSGlobalDomain = {
AppleInterfaceStyle = "Dark";
Expand Down Expand Up @@ -40,6 +50,9 @@
casks = [
"wezterm"
"claude-code"
"visual-studio-code@insiders"
"1password"
"1password-cli"
];
};
}
6 changes: 5 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
let
# The one username line to change if this isn't your machine.
# bootstrap.sh offers to rewrite this for you if your macOS username differs.
user = "kunchen";
user = "coder";
in
{
darwinConfigurations."mac" = nix-darwin.lib.darwinSystem {
Expand All @@ -32,6 +32,10 @@
home-manager.useUserPackages = true;
home-manager.extraSpecialArgs = { inherit user; };
home-manager.users.${user} = import ./home.nix;
# If a file home-manager wants to symlink already exists on disk
# (e.g. 1Password writes ~/.ssh/config itself when you enable its
# SSH agent), back it up instead of failing activation.
home-manager.backupFileExtension = "backup";
}
];
};
Expand Down
31 changes: 31 additions & 0 deletions home.nix
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,29 @@ in
fonts.fontconfig.enable = true;
home.sessionVariables.EDITOR = "nvim";

# GitHub auth and commit signing for this box. This machine runs
# unattended, so its keys are a dedicated pair pulled from a read-only
# 1Password Service Account into plain files at rebuild time (see
# rebuild.sh and README: "GitHub SSH authentication & commit signing"),
# rather than 1Password's interactive SSH agent, which always requires a
# human to approve each use. Signing therefore uses git's default
# ssh-keygen-based signer against the local key - no 1Password dependency
# at commit time, no prompts. user.name/email live in ~/.config/git/config-local
# instead, which rebuild.sh also materializes from 1Password - so this
# public repo's config never hardcodes anyone's real identity.
programs.git = {
enable = true;
includes = [
{ path = "${config.home.homeDirectory}/.config/git/config-local"; }
];
settings = {
user.signingkey = "${config.home.homeDirectory}/.ssh/id_ed25519_mac_signing";
commit.gpgsign = true;
gpg.format = "ssh";
gpg.ssh.allowedSignersFile = "${config.home.homeDirectory}/.ssh/allowed_signers";
};
};

programs.zsh = {
enable = true;
autosuggestion.enable = true; # ghost text from history
Expand Down Expand Up @@ -62,6 +85,14 @@ in
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/herdr";
home.file.".claude/settings.json".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.claude/settings.json";
home.file.".ssh/authorized_keys".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.ssh/authorized_keys";
home.file.".ssh/config".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.ssh/config";
# id_ed25519_mac_{auth,signing}(.pub) and allowed_signers are NOT declared
# here: they hold real key material (or are derived from it), so rebuild.sh
# writes them straight to ~/.ssh from 1Password via `op inject`, skipping
# both the Nix store (world-readable) and this git repo entirely.

# Keep Pi's credential and runtime state local by linking only authored files and directories.
home.file.".pi/agent/themes".source =
Expand Down
31 changes: 26 additions & 5 deletions home/.claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
{
"theme": "dark-ansi",
"attribution": {
"commit": "",
"pr": "",
"sessionUrl": false
},
"hooks": {
"SessionStart": [
{
"hooks": [
{
"command": "bash '/Users/coder/.claude/hooks/herdr-agent-state.sh' session",
"timeout": 10,
"type": "command"
}
],
"matcher": "*"
}
]
},
"model": "sonnet",
"statusLine": {
"type": "command",
"command": "input=$(cat); model=$(echo \"$input\" | jq -r '.model.display_name'); used=$(echo \"$input\" | jq -r '.context_window.used_percentage // empty'); if [ -n \"$used\" ]; then printf \"%s | ctx: %.0f%% used\" \"$model\" \"$used\"; else printf \"%s\" \"$model\"; fi"
}
}
"command": "input=$(cat); model=$(echo \"$input\" | jq -r '.model.display_name'); used=$(echo \"$input\" | jq -r '.context_window.used_percentage // empty'); if [ -n \"$used\" ]; then printf \"%s | ctx: %.0f%% used\" \"$model\" \"$used\"; else printf \"%s\" \"$model\"; fi",
"type": "command"
},
"theme": "dark-ansi",
"tui": "fullscreen"
}
3 changes: 3 additions & 0 deletions home/.config/git/config-local.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[user]
name = {{ op://mac-automation/dotfiles-personal/GIT_NAME }}
email = {{ op://mac-automation/dotfiles-personal/GIT_EMAIL }}
1 change: 1 addition & 0 deletions home/.config/herdr/config.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
onboarding = false
[keys]
prefix = "ctrl+b"
focus_pane_left = "prefix+h"
Expand Down
1 change: 1 addition & 0 deletions home/.ssh/allowed_signers.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sascha.s.krumbach@gmail.com {{ op://mac-automation/dotfiles-mac-signing/public key }}
9 changes: 9 additions & 0 deletions home/.ssh/authorized_keys
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Replace the placeholder below with your Windows PC's SSH public key.
#
# On the Windows PC, generate a keypair if you don't already have one:
# ssh-keygen -t ed25519
# Then paste the contents of the resulting .pub file here (one line, starting
# with "ssh-ed25519" or "ssh-rsa"), replacing the placeholder line, and run
# ./rebuild.sh on the Mac to apply it.
#
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJi8OUIM0Esj2JG+LYij8dqG+RlqNoWjavfk40tckhCi
13 changes: 13 additions & 0 deletions home/.ssh/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# github.com uses this machine's dedicated deploy-style key, materialized
# locally by rebuild.sh via a read-only 1Password Service Account (see
# README: "GitHub SSH authentication & commit signing"). No agent, no
# prompts - this box runs unattended and nobody is present to approve one.
Host github.com
IdentityFile ~/.ssh/id_ed25519_mac_auth
IdentitiesOnly yes
IdentityAgent none

# Everything else still goes through 1Password's interactive SSH agent, for
# times a human is actually sitting at (or VNC'd into) this machine.
Host *
IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
1 change: 1 addition & 0 deletions home/.ssh/id_ed25519_mac_auth.pub.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ op://mac-automation/dotfiles-mac-auth/public key }}
1 change: 1 addition & 0 deletions home/.ssh/id_ed25519_mac_auth.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ op://mac-automation/dotfiles-mac-auth/private key?ssh-format=openssh }}
1 change: 1 addition & 0 deletions home/.ssh/id_ed25519_mac_signing.pub.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ op://mac-automation/dotfiles-mac-signing/public key }}
1 change: 1 addition & 0 deletions home/.ssh/id_ed25519_mac_signing.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ op://mac-automation/dotfiles-mac-signing/private key?ssh-format=openssh }}
26 changes: 25 additions & 1 deletion rebuild.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,28 @@
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
ln -sfn "$DIR" ~/.dotfiles
exec sudo darwin-rebuild switch --flake ~/.dotfiles#mac
sudo darwin-rebuild switch --flake ~/.dotfiles#mac

# Materialize this machine's GitHub auth/signing keys from the
# "mac-automation" 1Password vault via a read-only Service Account. See
# README: "GitHub SSH authentication & commit signing". Skips quietly on a
# fresh clone before that token exists.
TOKEN_FILE="$HOME/.config/op/service-account-token"
if [[ -f "$TOKEN_FILE" ]]; then
export OP_SERVICE_ACCOUNT_TOKEN
OP_SERVICE_ACCOUNT_TOKEN="$(cat "$TOKEN_FILE")"
for name in id_ed25519_mac_auth id_ed25519_mac_signing; do
op inject --force -i "$DIR/home/.ssh/$name.tmpl" -o "$HOME/.ssh/$name"
op inject --force -i "$DIR/home/.ssh/$name.pub.tmpl" -o "$HOME/.ssh/$name.pub"
chmod 600 "$HOME/.ssh/$name"
chmod 644 "$HOME/.ssh/$name.pub"
done
op inject --force -i "$DIR/home/.ssh/allowed_signers.tmpl" -o "$HOME/.ssh/allowed_signers"
chmod 644 "$HOME/.ssh/allowed_signers"

mkdir -p "$HOME/.config/git"
op inject --force -i "$DIR/home/.config/git/config-local.tmpl" -o "$HOME/.config/git/config-local"
chmod 644 "$HOME/.config/git/config-local"
else
echo "note: $TOKEN_FILE not found - skipping 1Password key injection (see README)" >&2
fi