From ea7f712f715a8770e62e040bcbac049bbe37ba8c Mon Sep 17 00:00:00 2001 From: Josh Nichols Date: Fri, 12 Jun 2026 19:41:30 -0400 Subject: [PATCH 1/2] feat(git): verify SSH-signed commits locally via allowed_signers Add ~/.gitconfig.d/allowed_signers listing the personal human and personal-agent identities, and point gpg.ssh.allowedSignersFile at it from the signing fragment so both the human and agent gitconfigs inherit it. Without it, git log --show-signature can't verify SSH-signed commits (it errors that allowedSignersFile must be configured). GitHub's Verified badge is unaffected; this is local verification only. See ADR 0034. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...llowedsignersfile-for-local-commit-veri.md | 20 +++++ ...allowed-signers-for-commit-verification.md | 84 +++++++++++++++++++ home/.gitconfig.d/allowed_signers | 20 +++++ home/.gitconfig.d/signing | 4 + 4 files changed, 128 insertions(+) create mode 100644 .beans/dotfiles-wfq3--add-gpgsshallowedsignersfile-for-local-commit-veri.md create mode 100644 doc/adr/0034-local-ssh-allowed-signers-for-commit-verification.md create mode 100644 home/.gitconfig.d/allowed_signers diff --git a/.beans/dotfiles-wfq3--add-gpgsshallowedsignersfile-for-local-commit-veri.md b/.beans/dotfiles-wfq3--add-gpgsshallowedsignersfile-for-local-commit-veri.md new file mode 100644 index 0000000..30c70e6 --- /dev/null +++ b/.beans/dotfiles-wfq3--add-gpgsshallowedsignersfile-for-local-commit-veri.md @@ -0,0 +1,20 @@ +--- +# dotfiles-wfq3 +title: Add gpg.ssh.allowedSignersFile for local commit verification +status: completed +type: feature +priority: normal +created_at: 2026-06-12T23:16:08Z +updated_at: 2026-06-12T23:19:44Z +--- + +Added local SSH allowed-signers trust file so git can verify SSH-signed commits locally. + +## Checklist + +- [x] Create home/.gitconfig.d/allowed_signers with human + personal-agent identities +- [x] Add allowedSignersFile to home/.gitconfig.d/signing +- [x] Verify git log --show-signature reports Good signature (HEAD 7e45125 = G) +- [x] Write ADR 0034 + +Result: personal-agent + personal-human commits verify as G. Work-agent commits show U (No principal matched) until work identity is appended. GitHub PR-merge commits show E (GPG/RSA, not SSH). All expected. diff --git a/doc/adr/0034-local-ssh-allowed-signers-for-commit-verification.md b/doc/adr/0034-local-ssh-allowed-signers-for-commit-verification.md new file mode 100644 index 0000000..89f341f --- /dev/null +++ b/doc/adr/0034-local-ssh-allowed-signers-for-commit-verification.md @@ -0,0 +1,84 @@ +# 34. Local SSH allowed signers for commit verification + +Date: 2026-06-12 + +## Status + +Accepted + +## Context + +Commits are signed with SSH keys (`gpg.format = ssh`, see +[ADR 31](0031-role-scoped-agent-git-identity.md)). Signing only needs the +private key, so it works with no extra config. Verification is different. + +Unlike GPG, SSH signing has no keyservers or web of trust. A signature only +proves "some key signed this blob"; git can't map that key back to an identity +on its own. To verify, git needs an allowed-signers file that lists which +public keys it should trust for which committer emails. Without it, +`git log --show-signature` and `git verify-commit` fail with: + +``` +error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification +``` + +So locally, every signed commit shows as unverified, including the user's own. +GitHub is unaffected: it verifies server-side against the keys enrolled on the +account and shows the Verified badge regardless. The gap is purely local +verification. + +## Decision + +Manage a dotfiles-tracked allowed-signers file at +`home/.gitconfig.d/allowed_signers` (surfaced as `~/.gitconfig.d/allowed_signers` +via the symlinked `~/.gitconfig.d` directory) and point +`gpg.ssh.allowedSignersFile` at it from the `signing` fragment. + +Because the `signing` fragment is included by the generated `~/.gitconfig.local` +and the agent gitconfig (`claude-agent-`) `[include]`s `~/.gitconfig`, both +the human and agent identities inherit the setting from one place. + +The file lists one line per identity (` `): + +- `joshua.nichols@gmail.com` -- personal human (1Password-managed key) +- `joshua.nichols+personal-agent@gmail.com` -- personal agent (ADR 31) + +It is a plain data file, not a gitconfig fragment, and the dir is included +per-file (not globbed), so it is never parsed as config. Public keys are safe +to commit; the email is already present in `personal-identity`. + +### Why a single fleet-wide file rather than role-scoped + +Unlike the 1Password agent allowlist ([ADR 33](0033-1password-ssh-agent-allowlist.md)), +allowed-signers is additive: listing a key that never signs anything on this +host costs nothing. The same repo's history can contain commits from any role's +identity (e.g. a personal laptop checking out a branch with work-agent commits), +so one file listing every identity gives the most complete local verification. +Work-role identities (`josh.nichols+agent@gusto.com` and the work human key) +are appended to the same file when that role's keys are available. + +## Consequences + +### Positive + +- `git log --show-signature` / `verify-commit` confirm the user's own signed + commits locally (`Good "git" signature for `), no GitHub round-trip. +- One setting in the `signing` fragment covers both human and agent identities. +- No effect on signing, on the GitHub Verified badge, or on any enforcement; + git never blocks a commit, push, or merge based on this file. + +### Negative + +- The file is a second copy of public keys that must stay in sync with what is + enrolled on GitHub. Rotate a key and forget to update here, and local verify + breaks while GitHub keeps working, a confusing state to debug. +- Until work-role identities are added, work-agent commits in shared history + verify as `U` (good signature, `No principal matched`), not `G`. +- GPG-signed commits (e.g. GitHub PR-merge commits, signed with GitHub's RSA + key) still show as `E` under SSH verification; allowed-signers only covers SSH + signatures. + +## Links + +- Refines [ADR 31](0031-role-scoped-agent-git-identity.md) (agent SSH signing) +- Related [ADR 30](0030-ssh-keychain-loading-at-login.md) (silent key load) diff --git a/home/.gitconfig.d/allowed_signers b/home/.gitconfig.d/allowed_signers new file mode 100644 index 0000000..ac2436e --- /dev/null +++ b/home/.gitconfig.d/allowed_signers @@ -0,0 +1,20 @@ +# SSH allowed signers for local git commit/tag verification. +# Pointed at by gpg.ssh.allowedSignersFile in the `signing` fragment, so both +# the human gitconfig and the Claude agent gitconfig (claude-agent-, +# which [include]s ~/.gitconfig) inherit it. See ADR 0034. +# +# This is a LOCAL trust set only: it lets `git log --show-signature` and +# `git verify-commit` confirm a commit was signed by a key listed here. It has +# no effect on signing, on GitHub's Verified badge, or on enforcement (git +# never blocks anything based on this file). +# +# Format: +# A principal not listed here verifies as "Could not verify signature" (valid +# bytes, untrusted identity), NOT a bad/forged signature. Public keys are safe +# to commit. Append work-role identities here when that role is set up. + +# Personal human identity (1Password-managed signing key) +joshua.nichols@gmail.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMq3iskSznOUrNVpvTSVuXnXcRLtezOm9ey3X/TlnyhH + +# Personal-agent identity (Claude Code, ~/.ssh/agents/personal/id_ed25519, ADR 0031) +joshua.nichols+personal-agent@gmail.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFD+X13rkqG4oVpGXa1PevJVl/SgYWzZvrTyyc9CYSxT diff --git a/home/.gitconfig.d/signing b/home/.gitconfig.d/signing index 535fbe4..1b5062f 100644 --- a/home/.gitconfig.d/signing +++ b/home/.gitconfig.d/signing @@ -1,5 +1,9 @@ [gpg] format = ssh +[gpg "ssh"] + # Local trust set for verifying SSH-signed commits/tags. See ADR 0034. + allowedSignersFile = ~/.gitconfig.d/allowed_signers + [commit] gpgsign = true From e3e50273cf5f01b8520f02f0e684e125c3e2741c Mon Sep 17 00:00:00 2001 From: Josh Nichols Date: Fri, 12 Jun 2026 19:42:01 -0400 Subject: [PATCH 2/2] fix(claude): complete personal->home role rename with fail-loud guard DOTPICKLES_ROLE=home had no matching claude/roles/home.jsonc (only personal.jsonc existed), so claudeconfig.sh silently skipped the role merge, GIT_CONFIG_GLOBAL was never injected, and Claude agent commits fell back to ~/.gitconfig's op-ssh-sign -- a 1Password biometric prompt on every commit. The mismatch hid for ~2 months because a missing role file and a role with no overrides looked identical to the merge. Complete the personal->home rename across claudeconfig.sh (default + role file), install.sh, .zshenv, claude/roles/, and the agent gitconfig fragment, and warn loudly in claudeconfig.sh when a role has no matching file. The git identity keeps the personal-agent name (GitHub-enrolled); only the role changed. Document the canonical role names (ADR 0035) and the fail-loud decision (ADR 0036); correct stale references in ADR 0031 and architecture.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...le-name-convention-fail-loud-role-resol.md | 22 ++++++ ...l-home-role-mismatch-breaking-agent-git.md | 26 +++++++ claude/roles/{personal.jsonc => home.jsonc} | 11 ++- claudeconfig.sh | 10 ++- .../0031-role-scoped-agent-git-identity.md | 17 ++-- .../0035-canonical-dotpickles-role-names.md | 78 +++++++++++++++++++ doc/adr/0036-fail-loud-role-resolution.md | 71 +++++++++++++++++ doc/adr/README.md | 3 + doc/architecture.md | 6 +- ...laude-agent-personal => claude-agent-home} | 8 +- home/.zshenv | 2 +- install.sh | 2 +- 12 files changed, 240 insertions(+), 16 deletions(-) create mode 100644 .beans/dotfiles-413h--document-role-name-convention-fail-loud-role-resol.md create mode 100644 .beans/dotfiles-f8oz--fix-personal-home-role-mismatch-breaking-agent-git.md rename claude/roles/{personal.jsonc => home.jsonc} (64%) create mode 100644 doc/adr/0035-canonical-dotpickles-role-names.md create mode 100644 doc/adr/0036-fail-loud-role-resolution.md rename home/.gitconfig.d/{claude-agent-personal => claude-agent-home} (67%) diff --git a/.beans/dotfiles-413h--document-role-name-convention-fail-loud-role-resol.md b/.beans/dotfiles-413h--document-role-name-convention-fail-loud-role-resol.md new file mode 100644 index 0000000..7115ebe --- /dev/null +++ b/.beans/dotfiles-413h--document-role-name-convention-fail-loud-role-resol.md @@ -0,0 +1,22 @@ +--- +# dotfiles-413h +title: Document role-name convention + fail-loud role resolution (ADRs) and finish personal->home drift +status: completed +type: task +priority: high +created_at: 2026-06-12T23:34:48Z +updated_at: 2026-06-12T23:37:31Z +--- + +Captured the role-name convention as a foundational ADR and the fail-loud guard as a change, plus finished the personal->home drift in code/docs. + +## Checklist + +- [x] Fix install.sh: personal -> home (was still emitting the dead name) +- [x] Fix doc/architecture.md prose: personal -> home + detection/duplication note +- [x] Write ADR 0035: canonical DOTPICKLES_ROLE names (home/work/container, personal retired) +- [x] Write ADR 0036: fail-loud role resolution guard (amends 0031 + 0035) +- [x] Correct stale personal refs in ADR 0031 + amendment note +- [x] Update doc/adr/README.md index for 0035 + 0036 +- [x] Detection-duplication documented as accepted cross-shell wart in 0035/0036 (not a TODO) +- [x] Prettier clean on all touched markdown diff --git a/.beans/dotfiles-f8oz--fix-personal-home-role-mismatch-breaking-agent-git.md b/.beans/dotfiles-f8oz--fix-personal-home-role-mismatch-breaking-agent-git.md new file mode 100644 index 0000000..9f2fab6 --- /dev/null +++ b/.beans/dotfiles-f8oz--fix-personal-home-role-mismatch-breaking-agent-git.md @@ -0,0 +1,26 @@ +--- +# dotfiles-f8oz +title: Fix personal->home role mismatch breaking agent git identity +status: completed +type: bug +priority: high +created_at: 2026-06-12T01:53:13Z +updated_at: 2026-06-12T01:57:50Z +--- + +DOTPICKLES_ROLE=home had no matching claude/roles/home.jsonc (only personal.jsonc). claudeconfig.sh silently skipped the role merge, so ~/.claude/settings.json got no env block, GIT_CONFIG_GLOBAL was never set, and Claude git commits fell through to op-ssh-sign -> 1Password prompt every commit. Fixed by completing the personal->home role rename and adding a loud-fail guard. + +## Checklist + +- [x] git mv claude/roles/personal.jsonc -> home.jsonc +- [x] git mv home/.gitconfig.d/claude-agent-personal -> claude-agent-home +- [x] Update GIT_CONFIG_GLOBAL ref + comments in home.jsonc +- [x] Update comment in claude-agent-home (kept personal-agent identity) +- [x] Fix stale personal default in home/.zshenv +- [x] Fix stale DOTPICKLES_ROLE:personal in fish_variables (gitignored, local only) +- [x] claudeconfig.sh: default ROLE home + loud-fail guard for missing role file +- [x] Regenerate ~/.claude/settings.json (env.GIT_CONFIG_GLOBAL now present) +- [x] Relink not needed (~/.gitconfig.d is a symlinked dir) +- [x] Verified: ssh-keygen signs non-interactively, commit authored as personal-agent, no 1Password prompt + +Note: local git log --show-signature shows No signature because gpg.ssh.allowedSignersFile is unset; GitHub still shows Verified via enrolled key. Optional follow-up if local verification matters. diff --git a/claude/roles/personal.jsonc b/claude/roles/home.jsonc similarity index 64% rename from claude/roles/personal.jsonc rename to claude/roles/home.jsonc index e167f24..8c1cf1a 100644 --- a/claude/roles/personal.jsonc +++ b/claude/roles/home.jsonc @@ -1,9 +1,14 @@ { - // Personal role agent identity (see ADR 0031). - // Every Claude Code session under DOTPICKLES_ROLE=personal commits and + // Home role agent identity (see ADR 0031). + // Every Claude Code session under DOTPICKLES_ROLE=home commits and // pushes as a dedicated agent identity instead of the user's 1Password // GPG flow, which requires interactive approval. // + // The dotfiles role is "home"; the agent *identity* it uses is the + // long-standing "personal-agent" (GitHub-enrolled email + key under + // ~/.ssh/agents/personal/). Those stay named "personal" to preserve the + // Verified-badge enrollment. Only the role name is "home". + // // Prerequisite: run `bin/setup-agent-ssh-key personal` once and complete // the printed GitHub enrollment steps. // GIT_CONFIG_GLOBAL swaps in an agent-specific gitconfig for Claude-spawned @@ -12,7 +17,7 @@ // ssh-keygen). Leading ~/ is expanded by claudeconfig.sh before writing // settings.json (GIT_CONFIG_GLOBAL itself does not expand ~). "env": { - "GIT_CONFIG_GLOBAL": "~/.gitconfig.d/claude-agent-personal", + "GIT_CONFIG_GLOBAL": "~/.gitconfig.d/claude-agent-home", }, "sandbox": { diff --git a/claudeconfig.sh b/claudeconfig.sh index 0e31366..ccce64e 100755 --- a/claudeconfig.sh +++ b/claudeconfig.sh @@ -48,7 +48,7 @@ read_json() { } # Detect role (uses existing DOTPICKLES_ROLE from environment) -ROLE="${DOTPICKLES_ROLE:-personal}" +ROLE="${DOTPICKLES_ROLE:-home}" echo "Configuring Claude Code for role: $ROLE" # Ensure ~/.claude exists and symlink managed files @@ -139,6 +139,14 @@ generate_settings() { # --- Load active role (if not base) --- local role_file="$DIR/claude/roles/$ROLE.jsonc" + # Loud guard: a missing role file silently drops all role-specific settings + # (env like GIT_CONFIG_GLOBAL, sandbox rules). That used to fail quietly when + # DOTPICKLES_ROLE and the role filenames drifted apart. See ADR 0031. + if [ "$ROLE" != "base" ] && [ ! -f "$role_file" ]; then + echo " ⚠️ WARNING: role '$ROLE' has no role file ($role_file)." >&2 + echo " No role-specific env (e.g. GIT_CONFIG_GLOBAL) or sandbox rules will apply." >&2 + echo " Check DOTPICKLES_ROLE and claude/roles/ for a name mismatch." >&2 + fi if [ -f "$role_file" ] && [ "$ROLE" != "base" ]; then local role_json role_json=$(read_json "$role_file") diff --git a/doc/adr/0031-role-scoped-agent-git-identity.md b/doc/adr/0031-role-scoped-agent-git-identity.md index e6ed0d7..73110b8 100644 --- a/doc/adr/0031-role-scoped-agent-git-identity.md +++ b/doc/adr/0031-role-scoped-agent-git-identity.md @@ -6,6 +6,12 @@ Date: 2026-04-18 Accepted +Amended by [ADR 0035](0035-canonical-dotpickles-role-names.md) and +[ADR 0036](0036-fail-loud-role-resolution.md): the non-work role this ADR calls +"personal" was renamed to the canonical `home`. The git _identity_ keeps the +`personal-agent` name (GitHub-enrolled); only the role name changed. References +below have been updated to match. + ## Context Git commits in this repo (and everywhere else) are signed via 1Password's GPG @@ -55,10 +61,11 @@ override git identity per role. Every Claude Code session inherits its git identity from the active dotfiles role via environment variables set in `claude/roles/.jsonc`. -Under the personal role, Claude commits and pushes as +Under the home role, Claude commits and pushes as `joshua.nichols+personal-agent@gmail.com`, signed with a dedicated ed25519 key at `~/.ssh/agents/personal/id_ed25519`, with its passphrase in macOS -Keychain. The user's interactive shells are untouched. +Keychain. The user's interactive shells are untouched. (The identity and key +keep the `personal-agent`/`personal` name; only the role is `home`.) Under the work role, the same mechanism applied to a different identity: `josh.nichols+agent@gusto.com` (a verified plus-address on the user's @@ -91,18 +98,18 @@ distinct email to attribute commits to. The default email format is role's identity uses a different mailbox or domain (work role uses `josh.nichols+agent@gusto.com`). -**3. Per-role env injection.** `claude/roles/personal.jsonc` sets a single +**3. Per-role env injection.** `claude/roles/home.jsonc` sets a single env var pointing at an agent-specific gitconfig file: ```jsonc "env": { - "GIT_CONFIG_GLOBAL": "~/.gitconfig.d/claude-agent-personal" + "GIT_CONFIG_GLOBAL": "~/.gitconfig.d/claude-agent-home" } ``` `GIT_CONFIG_GLOBAL` tells git to read the pointed-at file instead of `~/.gitconfig` for every git command in the session. The target is a -symlinked dotfiles-managed file (`home/.gitconfig.d/claude-agent-personal` +symlinked dotfiles-managed file (`home/.gitconfig.d/claude-agent-home` in the repo, surfaced via `link_directory_contents home`): ```ini diff --git a/doc/adr/0035-canonical-dotpickles-role-names.md b/doc/adr/0035-canonical-dotpickles-role-names.md new file mode 100644 index 0000000..e12ac63 --- /dev/null +++ b/doc/adr/0035-canonical-dotpickles-role-names.md @@ -0,0 +1,78 @@ +# 35. Canonical DOTPICKLES_ROLE names + +Date: 2026-06-12 + +## Status + +Accepted + +## Context + +Role-based adaptation is the central architectural pattern of this repo (see +[architecture.md](../architecture.md)): a single `DOTPICKLES_ROLE` value selects +git identity, Brewfile additions, shell behavior, Claude settings, and SSH agent +allowlists. Despite being load-bearing for nearly every subsystem, the role +_system_ was only ever described in passing inside other ADRs ([0013](0013-claude-code-configuration-management.md), +[0031](0031-role-scoped-agent-git-identity.md), [0033](0033-1password-ssh-agent-allowlist.md)). +The set of valid role names was never written down anywhere authoritative. + +That gap caused a real, long-lived failure. An early non-work role named +`personal` was later renamed to `home` in some places (`config/fish/config.fish`, +the SSH agent allowlist) but not others (`install.sh`, `home/.zshenv`, +`claudeconfig.sh`, `claude/roles/personal.jsonc`, the agent gitconfig). Because +nothing declared the canonical name, each consumer drifted independently. The +live machines emitted `home` while `claude/roles/` only had `personal.jsonc`, so +the Claude agent git identity silently never loaded and every agent commit fell +back to the interactive 1Password signing prompt. It went unnoticed for roughly +two months. + +Role detection is also necessarily duplicated: `install.sh` (bash), +`config/fish/config.fish` (fish), and `home/.zshenv` (zsh) each re-implement the +same hostname check because the three shells can't share one snippet. Three +copies of a literal role string is exactly where drift creeps in. + +## Decision + +The canonical `DOTPICKLES_ROLE` values are: + +- **`home`** -- personal machines (the default for any non-work, non-container host) +- **`work`** -- hostnames matching `josh-nichols-*` +- **`container`** -- detected at runtime inside containers (Docker/lxc) + +`personal` is **retired**; it was the former name for `home`. No new code should +emit or branch on it. + +Every role-keyed consumer must use exactly these names. Known consumers: + +- `Brewfile.` +- `claude/roles/.jsonc` +- `config/1password/agent.toml.` ([ADR 0033](0033-1password-ssh-agent-allowlist.md)) +- the role default in `claudeconfig.sh`, `sshconfig.sh`, `install.sh` + +### Role name vs agent identity name + +The dotfiles _role_ is distinct from the git _identity_ a role uses. The `home` +role signs as the GitHub-enrolled `personal-agent` identity (email +`joshua.nichols+personal-agent@gmail.com`, key under `~/.ssh/agents/personal/`, +see [ADR 0031](0031-role-scoped-agent-git-identity.md)). Those keep the +`personal` name to preserve the Verified-badge enrollment. Renaming the role to +`home` does **not** rename the identity. The agent gitconfig fragment is +`claude-agent-home` (named for the role), but its contents point at the +`personal`-named identity on purpose. + +## Consequences + +### Positive + +- One authoritative list to check a new role-keyed file against. +- The role-vs-identity distinction is recorded, so "why does `home` use a + `personal` key" has a documented answer instead of looking like a bug. + +### Negative + +- Detection stays duplicated across three shells; this ADR documents the + canonical names but does not (cannot easily) DRY the detection itself. The + mitigation is the fail-loud guard in [ADR 0036](0036-fail-loud-role-resolution.md), + which makes a drifted/typo'd role visible instead of silent. +- Renaming a role in future touches every consumer above at once; there is no + single rename point. diff --git a/doc/adr/0036-fail-loud-role-resolution.md b/doc/adr/0036-fail-loud-role-resolution.md new file mode 100644 index 0000000..061f5cb --- /dev/null +++ b/doc/adr/0036-fail-loud-role-resolution.md @@ -0,0 +1,71 @@ +# 36. Fail-loud role resolution + +Date: 2026-06-12 + +## Status + +Accepted + +Amends [ADR 0031](0031-role-scoped-agent-git-identity.md) and builds on +[ADR 0035](0035-canonical-dotpickles-role-names.md). + +## Context + +`claudeconfig.sh` merges `claude/roles/base.jsonc` with a role file +(`claude/roles/$ROLE.jsonc`) and the stacks to generate `~/.claude/settings.json`. +The role file is where role-specific `env` (notably `GIT_CONFIG_GLOBAL`, which +swaps in the agent git identity per [ADR 0031](0031-role-scoped-agent-git-identity.md)) +and sandbox rules live. + +The role-loading step was guarded by `if [ -f "$role_file" ] && [ "$ROLE" != "base" ]`. +When `$ROLE` pointed at a name with no matching file, that condition was simply +false and the merge was skipped. A missing role file and a role that genuinely +has no overrides were **indistinguishable** to the script: both produced a +settings.json with no role `env` block. + +That silence is what let the `personal` -> `home` drift (see +[ADR 0035](0035-canonical-dotpickles-role-names.md)) hide for ~2 months. The +live role was `home`, `claude/roles/home.jsonc` didn't exist, the merge was +skipped, `GIT_CONFIG_GLOBAL` was never set, and the only symptom was 1Password +signing prompts during agent sessions, which looks exactly like normal +interactive behavior. Nothing pointed at the cause. + +## Decision + +`claudeconfig.sh` warns loudly when `$ROLE` is not `base` and has no matching +role file: + +``` +⚠️ WARNING: role '' has no role file (claude/roles/.jsonc). + No role-specific env (e.g. GIT_CONFIG_GLOBAL) or sandbox rules will apply. + Check DOTPICKLES_ROLE and claude/roles/ for a name mismatch. +``` + +The default role in `claudeconfig.sh` is also `home` (was `personal`), matching +the canonical names in [ADR 0035](0035-canonical-dotpickles-role-names.md). + +### Why warn rather than hard-fail + +The script still generates a usable settings.json from base + stacks; a missing +role file degrades the config rather than corrupting it. Exiting non-zero would +block the whole `install.sh` run over a recoverable condition. A loud warning +surfaces the mismatch on every regeneration without taking the rest of the +config setup down with it. + +## Consequences + +### Positive + +- A drifted or typo'd `DOTPICKLES_ROLE` is visible immediately on the next + `claudeconfig.sh` run, instead of manifesting weeks later as mysterious + 1Password prompts. +- Generalizes beyond this one bug: any future role-keyed env or sandbox rule + that fails to load now announces itself. + +### Negative + +- It is a warning, not an enforcement. A user who ignores the line still gets a + reduced config. The guard makes the failure _legible_, not impossible. +- Only `claudeconfig.sh` is guarded. Other role-keyed lookups (Brewfile, the SSH + agent allowlist) have their own skip-with-message behavior and are not unified + under one mechanism. diff --git a/doc/adr/README.md b/doc/adr/README.md index 40d38dd..993b73c 100644 --- a/doc/adr/README.md +++ b/doc/adr/README.md @@ -33,3 +33,6 @@ - [31. role-scoped-agent-git-identity](0031-role-scoped-agent-git-identity.md) - [32. use-fnox-for-secrets](0032-use-fnox-for-secrets.md) - [33. 1password-ssh-agent-allowlist](0033-1password-ssh-agent-allowlist.md) +- [34. local-ssh-allowed-signers-for-commit-verification](0034-local-ssh-allowed-signers-for-commit-verification.md) +- [35. canonical-dotpickles-role-names](0035-canonical-dotpickles-role-names.md) +- [36. fail-loud-role-resolution](0036-fail-loud-role-resolution.md) diff --git a/doc/architecture.md b/doc/architecture.md index 1f33411..c3068e6 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -8,7 +8,7 @@ The central architectural pattern is **role-based adaptation**. The role is dete - **Brewfile selection**: `Brewfile` + `Brewfile.$ROLE` are merged during brew bundle - **Shell environment**: Various configs conditionally load based on role -Role detection logic is in [install.sh:12-23](../install.sh#L12-L23). The role defaults to "work" for hostnames matching `josh-nichols-*`, otherwise "personal". +The canonical role values are `home`, `work`, and `container` (see [ADR 0035](adr/0035-canonical-dotpickles-role-names.md)). Detection defaults to `work` for hostnames matching `josh-nichols-*`, `container` inside containers, otherwise `home`. The same hostname check is duplicated across [install.sh](../install.sh), [config/fish/config.fish](../config/fish/config.fish), and [home/.zshenv](../home/.zshenv) because bash, fish, and zsh can't share one snippet; the canonical-name list and a fail-loud guard ([ADR 0036](adr/0036-fail-loud-role-resolution.md)) keep the copies from drifting silently. ## Symlink-Based File Management @@ -29,8 +29,8 @@ Git settings are **generated**, not static. [gitconfig.sh](../gitconfig.sh) rebu 3. Conditionally including config fragments from `home/.gitconfig.d/` based on: - Available commands (delta, gh, git-duet, fzf, code/code-insiders) - Operating system (macOS) - - Role (personal/work) - - 1Password availability (for SSH signing on personal) + - Role (home/work) + - 1Password availability (for SSH signing on home) `~/.gitconfig.local` should never be edited manually or committed. The base config at [home/.gitconfig](../home/.gitconfig) includes this generated file. diff --git a/home/.gitconfig.d/claude-agent-personal b/home/.gitconfig.d/claude-agent-home similarity index 67% rename from home/.gitconfig.d/claude-agent-personal rename to home/.gitconfig.d/claude-agent-home index 39e2f01..2c8e2f0 100644 --- a/home/.gitconfig.d/claude-agent-personal +++ b/home/.gitconfig.d/claude-agent-home @@ -1,7 +1,11 @@ -# Agent git identity for Claude Code under DOTPICKLES_ROLE=personal. +# Agent git identity for Claude Code under DOTPICKLES_ROLE=home. # See doc/adr/0031-role-scoped-agent-git-identity.md. # -# Referenced via GIT_CONFIG_GLOBAL in claude/roles/personal.jsonc. The +# The role is "home"; the identity is the GitHub-enrolled "personal-agent" +# (email + key under ~/.ssh/agents/personal/), kept named "personal" to +# preserve the Verified-badge enrollment. +# +# Referenced via GIT_CONFIG_GLOBAL in claude/roles/home.jsonc. The # [include] pulls in the real global gitconfig first (transitively pulling # in gitconfig.d fragments via ~/.gitconfig's own includes). Later sections # here override: user.email, core.sshCommand for the agent SSH key, and diff --git a/home/.zshenv b/home/.zshenv index d02628a..9aedb3b 100644 --- a/home/.zshenv +++ b/home/.zshenv @@ -16,7 +16,7 @@ if [[ -f /.dockerenv ]] || grep -q 'docker\|lxc\|containerd' /proc/1/cgroup 2> / elif [[ "$(hostname)" =~ ^josh-nichols- ]]; then export DOTPICKLES_ROLE=work else - export DOTPICKLES_ROLE=personal + export DOTPICKLES_ROLE=home fi # Load local environment customizations if present diff --git a/install.sh b/install.sh index 0f999eb..aa4554d 100755 --- a/install.sh +++ b/install.sh @@ -31,7 +31,7 @@ if [[ -z "${DOTPICKLES_ROLE}" ]]; then if [[ "$hostname" =~ ^josh-nichols- ]]; then DOTPICKLES_ROLE="work" else - DOTPICKLES_ROLE="personal" + DOTPICKLES_ROLE="home" fi fi