From 9c5bf7e8d75b4bd90eccad6b1badb2d60bd6e8be Mon Sep 17 00:00:00 2001 From: ngockhoi96 Date: Mon, 7 Sep 2026 18:38:05 +0700 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat(claude):=20multi-account?= =?UTF-8?q?=20via=20CLAUDE=5FCONFIG=5FDIR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run a personal subscription and a company Teams account on one machine with no collision. Three fish wrappers key CLAUDE_CONFIG_DIR off the same ghqCompanyRoot boundary git's includeIf uses: - claude: auto — work (~/.claude.work) inside the company root, personal (~/.claude default) elsewhere - claude-work / claude-personal: explicit overrides Asymmetric layout (personal stays the stock ~/.claude, no migration). Config dirs and their plaintext credentials stay untracked in $HOME; the company path stays templated. Guide in docs/claude-code.md, rationale in ADR 0023. --- README.md | 1 + ...ulti-account-claude-code-via-config-dir.md | 50 +++++++ docs/claude-code.md | 123 ++++++++++++++++++ .../fish/functions/claude-personal.fish | 6 + .../fish/functions/claude-work.fish | 7 + .../fish/functions/claude.fish.tmpl | 12 ++ 6 files changed, 199 insertions(+) create mode 100644 docs/adr/0023-multi-account-claude-code-via-config-dir.md create mode 100644 docs/claude-code.md create mode 100644 home/dot_config/fish/functions/claude-personal.fish create mode 100644 home/dot_config/fish/functions/claude-work.fish create mode 100644 home/dot_config/fish/functions/claude.fish.tmpl diff --git a/README.md b/README.md index a4b7a3a..c07dcd6 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ Each tool has a focused guide covering **what it is, why, and how to set it up** | [ghq.md](docs/ghq.md) | Organized repo cloning + fuzzy jumping | | [ssh.md](docs/ssh.md) | SSH keys per host (personal + work auth) | | [git.md](docs/git.md) | Git identities, `includeIf`, SSH commit signing | +| [claude-code.md](docs/claude-code.md) | Personal + company Teams accounts, one machine (`CLAUDE_CONFIG_DIR`) | | [gopass.md](docs/gopass.md) | Terminal password manager (GPG + git) | | [certs.md](docs/certs.md) | Corporate CA certs — trust store restore | | [vpn.md](docs/vpn.md) | Corporate VPN (GlobalProtect / `gpclient`) | diff --git a/docs/adr/0023-multi-account-claude-code-via-config-dir.md b/docs/adr/0023-multi-account-claude-code-via-config-dir.md new file mode 100644 index 0000000..5f8cb23 --- /dev/null +++ b/docs/adr/0023-multi-account-claude-code-via-config-dir.md @@ -0,0 +1,50 @@ +# Multiple Claude Code accounts via `CLAUDE_CONFIG_DIR`, keyed on the git company root + +**Status:** accepted + +We run a personal Claude subscription and a company **Teams** account on the same +machine. The Teams account carries the company's data-retention/privacy policy, so +company work must not run under the personal login. Claude Code has no account +switcher, but `CLAUDE_CONFIG_DIR` relocates an account's entire state — credential, +`projects/` history, skills, plugins, settings — so two dirs give two isolated +accounts. + +## Decision + +Drive `CLAUDE_CONFIG_DIR` from a directory boundary with three autoloaded fish +wrappers. `claude` auto-selects: inside the company ghq root it exports +`~/.claude.work`, elsewhere it uses the personal default `~/.claude`; `claude-work` +and `claude-personal` force a choice. The boundary is the **same** +`{{ .ghqCompanyRoot }}` template variable git's `includeIf` uses — one definition of +"this path is work" for both tools. + +Choices worth recording: + +- **Asymmetric dirs.** Personal stays the stock `~/.claude` (already logged in, + no migration); only `~/.claude.work` is added. The symmetric alternative + (`~/.claude.personal` + `~/.claude.work`, with an empty `~/.claude` as a + "you forgot to pick" tripwire) was rejected as needless re-login + history + migration for a fail-safe that a terminal-only launch path doesn't need. +- **fish wrapper, not direnv.** A wrapper needs zero per-repo files and covers the + real launch path (the terminal). direnv would be editor-proof but wants a + `.envrc` + `direnv allow` under the company root; recorded as the upgrade for if + Claude ever gets launched from an editor. +- **Porous boundary, deliberately.** Skills and the statusline are shared across + both accounts; `CLAUDE.md` (`@RTK.md`), `memory/`, history, credentials, and the + RTK hook stay personal-only. `settings.json` is therefore _not_ shared — it + carries the RTK command-rewrite hook — so the work dir gets its own hook-free + `settings.json`, keeping RTK off every company session by construction. + +## Consequences + +- The company path never enters the public repo: `claude.fish.tmpl` renders + `{{ .ghqCompanyRoot }}` per machine, and both config dirs (with plaintext + `.credentials.json`) live untracked in `$HOME` — consistent with + [ADR 0002](0002-private-data-via-templates.md). +- Only terminal launches auto-switch; an editor-launched session falls back to + personal. Acceptable until something launches Claude outside fish, at which point + add a company-root `.envrc`. +- The account signal depends on the statusline inheriting `CLAUDE_CONFIG_DIR` from + the wrapper; if a future build stops passing it through, export a dedicated + `CLAUDE_ACCOUNT` instead. +- Full guide: [docs/claude-code.md](../claude-code.md). diff --git a/docs/claude-code.md b/docs/claude-code.md new file mode 100644 index 0000000..22dd9ee --- /dev/null +++ b/docs/claude-code.md @@ -0,0 +1,123 @@ +# Claude Code — one machine, two accounts + +Run a **personal** Claude subscription and a **company Teams** account on the same +box without them ever colliding — the same "one machine, two identities" idea as +[git.md](git.md), applied to Claude Code. + +## Why + +The company Teams account has its own data-retention and privacy policy, so +company work must run under it — not under the personal login. But there is no +`/switch` command and no profile picker in Claude Code. What it does have is one +environment variable, `CLAUDE_CONFIG_DIR`, that relocates **everything** for an +account: the OAuth credential (`.credentials.json`), the full session history +(`projects/`), skills, plugins, and settings. Point two shells at two dirs and +you get two fully isolated accounts. + +We drive that variable from a directory boundary — the **same** `ghqCompanyRoot` +that git's `includeIf` already uses — so the account follows where the repo lives, +with zero manual switching. No third-party tool; the wrappers are ~10 lines of fish. + +## The layout — asymmetric on purpose + +| Account | Config dir | How it's reached | +| ------------ | --------------------- | --------------------------------------------------- | +| **personal** | `~/.claude` (default) | anything outside the company root; `command claude` | +| **work** | `~/.claude.work` | inside the company root; `claude-work` | + +Personal stays as the stock `~/.claude` — already logged in, nothing to migrate. +Only `~/.claude.work` is new. Neither dir is tracked by chezmoi; both live in +`$HOME` and hold a **plaintext** `.credentials.json` (mode 0600) — they must never +enter this public repo. + +## The wrappers + +Three autoloaded fish functions (`home/dot_config/fish/functions/`): + +- **`claude`** — auto. If `$PWD` is inside `{{ .ghqCompanyRoot }}` it exports + `CLAUDE_CONFIG_DIR=~/.claude.work`; otherwise it runs the personal default. + `claude.fish.tmpl` is templated so the company path never lands in the repo. +- **`claude-work`** — force the work account from anywhere. +- **`claude-personal`** — force personal even inside a work dir (`env -u + CLAUDE_CONFIG_DIR`). + +Fish has no `VAR=val cmd` prefix syntax, so the wrappers use `set -lx` (local + +exported, function-scoped) and `env -u`. Each forwards `$argv` verbatim, so every +Claude flag and subcommand passes straight through. + +> **Terminal only.** A fish function shadows `claude` only in an interactive +> shell. If you ever launch Claude Code from an editor (Zed's agent, say), the +> wrapper is bypassed and you get the default `~/.claude` (personal). To make the +> boundary editor-proof, drop a `.envrc` under the company root that exports +> `CLAUDE_CONFIG_DIR` — see [direnv.md](direnv.md). Not needed today: nothing +> launches Claude outside the terminal. + +## What crosses the boundary (and what doesn't) + +Everything inside a config dir is per-account, so `~/.claude.work` is a blank +slate. The chosen split: + +| Shared into work | Kept personal-only | +| ---------------------------------- | ------------------------------------------------ | +| `skills/` (symlink) | `CLAUDE.md` (`@RTK.md`) — RTK is a personal tool | +| statusline script (same abs. path) | `memory/` — personal auto-memory | +| | `projects/` — session history | +| | `.credentials.json` — the login itself | +| | the **RTK hook** (lives in `settings.json`) | + +`settings.json` is **not** shared: it carries the RTK command-rewrite hook, so the +work dir gets its own minimal `settings.json` (statusline block only, no hook). +That keeps RTK — and its token proxy — off every company session by construction. + +## Bootstrap (once, machine-local — not committed) + +```sh +# 1. Apply the dotfiles so the fish wrappers exist +chezmoi apply + +# 2. Create the work dir and log in with the company Teams account +mkdir -p ~/.claude.work +claude-work # browser OAuth flow → approve as the company identity + +# 3. Share skills; keep everything else separate +ln -s ~/.claude/skills ~/.claude.work/skills + +# 4. Give the work dir a settings.json with the statusline but NO RTK hook. +# Point it at the same absolute script the personal one uses: +# "statusLine": { "type": "command", +# "command": "bash /home//.claude/statusline-command.sh" } +``` + +## Knowing which account you're in + +The statusline (`~/.claude/statusline-command.sh`, shared by both dirs) reads +`$CLAUDE_CONFIG_DIR` and leads with a colored tag — green **personal**, red +**work** — so every session, auto or forced, shows its account. That variable is +exported by the wrapper and inherited by the statusline subprocess. + +> Verify inheritance once: run `claude-work`, and check the tag is red. If your +> Claude build doesn't pass `CLAUDE_CONFIG_DIR` through to the statusline, have +> the wrappers also export a plain `CLAUDE_ACCOUNT` and read that instead. + +## What's tracked (and what isn't) + +**Tracked** (public repo, no secrets): the three fish functions and this guide. +`claude.fish.tmpl` renders `{{ .ghqCompanyRoot }}` per machine, so the company +path stays out of the source — same pattern as [git.md](git.md) and +[vpn.md](vpn.md). + +**Never tracked** (machine-local `$HOME`): `~/.claude`, `~/.claude.work`, both +`.credentials.json`, all history, the statusline script, and the work +`settings.json`. + +## Related + +- [git.md](git.md) — the same personal/work identity split for git (`includeIf`) +- [ghq.md](ghq.md) — where the company root (`ghqCompanyRoot`) comes from +- [direnv.md](direnv.md) — path-based env, the editor-proof upgrade +- [ADR 0023](adr/0023-multi-account-claude-code-via-config-dir.md) — why this design +- [ADR 0002](adr/0002-private-data-via-templates.md) — private paths via templates + +## References + +- Claude Code — [`CLAUDE_CONFIG_DIR` / settings](https://docs.claude.com/en/docs/claude-code/settings) diff --git a/home/dot_config/fish/functions/claude-personal.fish b/home/dot_config/fish/functions/claude-personal.fish new file mode 100644 index 0000000..fc5a91a --- /dev/null +++ b/home/dot_config/fish/functions/claude-personal.fish @@ -0,0 +1,6 @@ +function claude-personal --description "Claude Code — force the personal account (default ~/.claude), even inside a work dir" + # Explicit override: strip any inherited CLAUDE_CONFIG_DIR so Claude falls + # back to the personal default ~/.claude. `env` runs the real binary (not + # this shell's functions), so there is no recursion. See docs/claude-code.md. + env -u CLAUDE_CONFIG_DIR claude $argv +end diff --git a/home/dot_config/fish/functions/claude-work.fish b/home/dot_config/fish/functions/claude-work.fish new file mode 100644 index 0000000..c3471dd --- /dev/null +++ b/home/dot_config/fish/functions/claude-work.fish @@ -0,0 +1,7 @@ +function claude-work --description "Claude Code — force the work (Teams) account, any directory" + # Explicit override for the auto-detecting `claude` function: always use the + # work config dir (~/.claude.work — separate login, history, skills, no RTK + # hook). See docs/claude-code.md. + set -lx CLAUDE_CONFIG_DIR $HOME/.claude.work + command claude $argv +end diff --git a/home/dot_config/fish/functions/claude.fish.tmpl b/home/dot_config/fish/functions/claude.fish.tmpl new file mode 100644 index 0000000..753141b --- /dev/null +++ b/home/dot_config/fish/functions/claude.fish.tmpl @@ -0,0 +1,12 @@ +function claude --description "Claude Code — auto-select account by directory (work inside the company ghq root)" + # Mirror the git `includeIf` boundary: inside the company ghq root, run the + # work (Teams) account from ~/.claude.work; everywhere else use the personal + # default (~/.claude). One boundary — {{ .ghqCompanyRoot }} — drives both the + # git identity and the Claude account, so moving the company root moves both. + # Force a choice with `claude-work` / `claude-personal`. See docs/claude-code.md. + set -l company_root (string replace -r '^~' $HOME -- '{{ .ghqCompanyRoot }}') + if string match -q -- "$company_root/*" "$PWD/" + set -lx CLAUDE_CONFIG_DIR $HOME/.claude.work + end + command claude $argv +end From 00338e2f48c06e84f2dcd196e195d991795c25fb Mon Sep 17 00:00:00 2001 From: ngockhoi96 Date: Wed, 9 Sep 2026 14:00:24 +0700 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20feat(claude):=20track=20statusl?= =?UTF-8?q?ine,=20global=20instructions=20&=20git-workflow=20skill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vendor the personal Claude Code config worth reproducing on a new machine: the statusline script, the global CLAUDE.md (@RTK.md) + RTK.md, and the hand-authored git-workflow skill (now a user-global skill, was project-local). settings.json is deliberately left untracked (app rewrites it → snapshot drift, per ADR 0012); docs/claude-code.md carries a copy-paste bootstrap for the statusLine wiring, RTK hook, plugin marketplaces, and the npx-skills install. --- docs/claude-code.md | 98 +++++- home/dot_claude/CLAUDE.md | 1 + home/dot_claude/RTK.md | 29 ++ home/dot_claude/skills/git-workflow/SKILL.md | 201 +++++++++++++ .../skills/git-workflow/evals/evals.json | 75 +++++ .../references/commit-conventions.md | 203 +++++++++++++ .../references/commit-workflow.md | 163 ++++++++++ .../references/hooks-and-tools.md | 284 ++++++++++++++++++ .../git-workflow/references/security.md | 227 ++++++++++++++ home/dot_claude/statusline-command.sh | 106 +++++++ 10 files changed, 1379 insertions(+), 8 deletions(-) create mode 100644 home/dot_claude/CLAUDE.md create mode 100644 home/dot_claude/RTK.md create mode 100644 home/dot_claude/skills/git-workflow/SKILL.md create mode 100644 home/dot_claude/skills/git-workflow/evals/evals.json create mode 100644 home/dot_claude/skills/git-workflow/references/commit-conventions.md create mode 100644 home/dot_claude/skills/git-workflow/references/commit-workflow.md create mode 100644 home/dot_claude/skills/git-workflow/references/hooks-and-tools.md create mode 100644 home/dot_claude/skills/git-workflow/references/security.md create mode 100644 home/dot_claude/statusline-command.sh diff --git a/docs/claude-code.md b/docs/claude-code.md index 22dd9ee..552e165 100644 --- a/docs/claude-code.md +++ b/docs/claude-code.md @@ -88,6 +88,67 @@ ln -s ~/.claude/skills ~/.claude.work/skills # "command": "bash /home//.claude/statusline-command.sh" } ``` +## Personal account bootstrap (new machine) + +`chezmoi apply` drops `~/.claude/statusline-command.sh`, `~/.claude/CLAUDE.md`, +and `~/.claude/RTK.md` — but **not** `settings.json` (see +[What's tracked](#whats-tracked-and-what-isnt) for why). Two manual steps wire up +the rest. + +1. Point `settings.json` at the statusline and re-add the RTK hook — merge into + `~/.claude/settings.json` (`` = your `$HOME` user): + + ```jsonc + { + "statusLine": { + "type": "command", + "command": "bash /home//.claude/statusline-command.sh" + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [{ "type": "command", "command": "rtk hook claude" }] + } + ] + } + } + ``` + +2. Re-add the plugin marketplaces and enable the plugins through `/plugin` (the + enabled set lives in the untracked `settings.json`): + + | Marketplace | Source | Plugins enabled from it | + | ------------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | + | `claude-plugins-official` | built-in | skill-creator, context7, code-simplifier, claude-md-management, gopls-lsp, playwright, mattpocock-skills | + | `karpathy-skills` | [forrestchang/andrej-karpathy-skills](https://github.com/forrestchang/andrej-karpathy-skills) | andrej-karpathy-skills | + | `cloudflare` | [cloudflare/skills](https://github.com/cloudflare/skills) | (skills only — cloudflare, wrangler, workers…) | + | `svelte` | [sveltejs/ai-tools](https://github.com/sveltejs/ai-tools) | (none enabled yet — kept as a known marketplace) | + +3. Re-install the cross-agent skills. These come from the `npx skills` manager + (state in `~/.agents/.skill-lock.json`), **not** the Claude plugins — the + Claude plugin for the same author exposes a _different_ set and does not + include these (`caveman`, `grill-with-docs`, …), so a plugin can't replace + them: + + ```sh + # caveman, grill-with-docs, handoff, teach, write-a-skill, improve-codebase-architecture + npx skills@latest add mattpocock/skills + ``` + + See [mattpocock/skills → Get the skills](https://github.com/mattpocock/skills#1-get-the-skills). + +> **Skills use two install paths, on purpose.** Claude _plugins_ are Claude-only; +> the `npx skills` manager symlinks a skill into every agent's dir (`~/.agents` is +> its shared home), which is why cross-agent skills go through it. The one +> hand-authored skill — [`git-workflow`](#whats-tracked-and-what-isnt) — is +> vendored into this repo instead (it encodes this repo's own commit +> conventions). `caveman` is the skill layer only; the token-**proxy** layer is +> owned by RTK, so [JuliusBrussee/caveman](https://github.com/JuliusBrussee/caveman)'s +> proxy is deliberately not installed (it would double up on RTK). If Codex (or +> any second agent) is adopted, relocate `git-workflow` to `~/.agents/skills` and +> symlink it in, matching the manager's own layout. + ## Knowing which account you're in The statusline (`~/.claude/statusline-command.sh`, shared by both dirs) reads @@ -101,14 +162,35 @@ exported by the wrapper and inherited by the statusline subprocess. ## What's tracked (and what isn't) -**Tracked** (public repo, no secrets): the three fish functions and this guide. -`claude.fish.tmpl` renders `{{ .ghqCompanyRoot }}` per machine, so the company -path stays out of the source — same pattern as [git.md](git.md) and -[vpn.md](vpn.md). - -**Never tracked** (machine-local `$HOME`): `~/.claude`, `~/.claude.work`, both -`.credentials.json`, all history, the statusline script, and the work -`settings.json`. +**Tracked** (public repo, no secrets): + +- the three fish functions and this guide — `claude.fish.tmpl` renders + `{{ .ghqCompanyRoot }}` per machine, so the company path stays out of the + source (same pattern as [git.md](git.md) and [vpn.md](vpn.md)); +- `home/dot_claude/statusline-command.sh` → `~/.claude/statusline-command.sh` — a + static, self-authored script with no per-machine value (it only reads + `$CLAUDE_CONFIG_DIR` and matches a generic `*/.claude.work`); +- `home/dot_claude/CLAUDE.md` (`@RTK.md`) and `RTK.md` — the personal global + instructions. Secret-free and portable; the RTK proxy they describe is already + tracked under [`dot_config/rtk`](../home/dot_config/rtk); +- `home/dot_claude/skills/git-workflow/` → `~/.claude/skills/git-workflow` — the + one hand-authored, repo-agnostic skill (it encodes this repo's own commit + conventions), so it is vendored and available in every project. Everything else + under `skills/` is installed, not authored (see below). + +**Never tracked** (machine-local `$HOME`, or trivially reinstalled): both +`.credentials.json`, all `projects/` history and `memory/`, the installed +`plugins/` and the rest of `skills/` (plugin dirs, or `npx skills` symlinks into +`~/.agents` — reinstalled from their source repos, see the bootstrap above), and +both `settings.json`. + +`settings.json` is left out on purpose. Claude Code rewrites it behind your back — +a theme toggle, enabling a plugin — so tracking it would mean the same +app-owned-snapshot drift as noctalia +([ADR 0012](adr/0012-noctalia-config-tracked-as-app-owned-snapshot.md)): +re-syncing source from the live file after every UI change. The little worth +reproducing (the statusline wiring, the RTK hook, the plugin list) is captured as +a copy-paste bootstrap above instead. ## Related diff --git a/home/dot_claude/CLAUDE.md b/home/dot_claude/CLAUDE.md new file mode 100644 index 0000000..351259c --- /dev/null +++ b/home/dot_claude/CLAUDE.md @@ -0,0 +1 @@ +@RTK.md diff --git a/home/dot_claude/RTK.md b/home/dot_claude/RTK.md new file mode 100644 index 0000000..0eaf3d5 --- /dev/null +++ b/home/dot_claude/RTK.md @@ -0,0 +1,29 @@ +# RTK - Rust Token Killer + +**Usage**: Token-optimized CLI proxy (60-90% savings on dev operations) + +## Meta Commands (always use rtk directly) + +```bash +rtk gain # Show token savings analytics +rtk gain --history # Show command usage history with savings +rtk discover # Analyze Claude Code history for missed opportunities +rtk proxy # Execute raw command without filtering (for debugging) +``` + +## Installation Verification + +```bash +rtk --version # Should show: rtk X.Y.Z +rtk gain # Should work (not "command not found") +which rtk # Verify correct binary +``` + +⚠️ **Name collision**: If `rtk gain` fails, you may have reachingforthejack/rtk (Rust Type Kit) installed instead. + +## Hook-Based Usage + +All other commands are automatically rewritten by the Claude Code hook. +Example: `git status` → `rtk git status` (transparent, 0 tokens overhead) + +Refer to CLAUDE.md for full command reference. diff --git a/home/dot_claude/skills/git-workflow/SKILL.md b/home/dot_claude/skills/git-workflow/SKILL.md new file mode 100644 index 0000000..e1ec87b --- /dev/null +++ b/home/dot_claude/skills/git-workflow/SKILL.md @@ -0,0 +1,201 @@ +--- +name: git-workflow +description: > + Analyze git changes and generate Conventional Commit messages with gitmoji + emoji prefixes. Handles intelligent staging, splitting multi-concern changes + into atomic commits, and executing the commit. Also covers git hook setup + (lefthook), secret scanning (gitleaks), and commitlint/cz-git configuration. + + Trigger whenever user says "commit", "git commit", "stage and commit", "gc", + "push changes", or asks about commit message format. Also trigger when user + wants to setup git hooks, scan for secrets, configure commitlint or cz-git, + or asks about any git workflow conventions — even if they don't use these + exact words. If the user mentions lefthook, gitleaks, commitlint, cz-git, + gitmoji, or conventional commits, use this skill. +--- + +# Git Workflow + +Two modes: **Commit** (daily, ~90% of use) and **Setup** (one-time config). +Detect which one the user needs from context and jump straight in. + +--- + +## Mode 1 — Commit + +### Step 1: Gather context + +Run in parallel: + +```bash +git status --short +git diff HEAD +git log --oneline -5 +``` + +### Step 2: Analyze and group + +Group changed files by concern. For each group: + +- **What** changed (files + nature of change) +- **Why** it changed (infer from diff content) +- **Which type** fits (see table below) + +**Split decision:** + +| Single commit | Split into multiple | +| ---------------------------------------- | ----------------------------------- | +| Same type AND scope | Mixed types (feat + fix) | +| ≤3 files | Mixed scopes (auth + billing) | +| ≤50 lines total diff | >10 files across unrelated areas | +| Splitting would break intermediate state | Deps mixed with code changes | +| | Formatting mixed with logic changes | + +### Step 3: Build commit message + +**Format:** `emoji type(scope): subject` + +**Type → Emoji mapping:** + +| Type | Emoji | When to use | +| ------------- | ----- | ------------------------------------ | +| `init` | 🎉 | Project kickoff | +| `feat` | ✨ | New user-facing feature | +| `fix` | 🐛 | Bug fix | +| `hotfix` | 🚑️ | Critical production fix | +| `docs` | 📝 | Documentation only | +| `style` | 💄 | CSS/visual changes, formatting | +| `refactor` | ♻️ | Code restructure, no behavior change | +| `perf` | ⚡️ | Performance improvement | +| `test` | ✅ | Add or update tests only | +| `update-deps` | ⬆️ | Dependency upgrades | +| `configs` | 🔧 | Config file changes | +| `chore` | 🔨 | Maintenance, tooling, other | +| `breaking` | 💥 | Breaking changes | +| `deploy` | 🚀 | Deployment | + +Pick the **highest-priority type** that matches. Priority: feat > fix > refactor > perf > test > docs > chore. + +**Scope rules:** + +- Lowercase, single word or hyphenated: `auth`, `user-profile`, `api` +- Derive from file paths: all files under `src/auth/` → scope `auth` +- Omit if changes span many unrelated areas +- If user specifies a ticket/issue reference, use footer format (see below) + +**Subject rules:** + +- Imperative mood: "add", "fix", "remove" — not "added", "fixed", "removed" +- Lowercase first letter +- No period at end +- ≤100 chars total header length + +**Body** — include when subject alone doesn't explain why: + +- Separate from subject with one blank line +- Each bullet starts with a verb (Add, Fix, Remove, Update, Refactor, etc.) +- One fact per bullet — no filler +- Explain **why**, not **what** (the diff shows what) + +**Footer** — for issue/ticket references: + +- Format: `Closes: #123` or `Refs: ELWB-1234` +- Only include when user mentions a ticket or the commit clearly closes an issue +- Multiple issues: one per line + +**Never include:** + +- AI attribution (no "Co-Authored-By", "Generated by", etc.) +- The commit author field already tracks who committed + +### Step 4: Propose to user + +Display the full message in a code block: + +``` +✨ feat(auth): add OAuth2 PKCE flow + +- Implement authorization code flow for mobile clients +- Add token exchange using oauth2-client library + +Closes: #214 +``` + +Ask: **"Stage and commit with this message? (yes / edit / cancel)"** + +- **yes** → proceed to Step 5 +- **edit** → ask what to change, revise, show again +- **cancel** → stop + +### Step 5: Execute + +**Staging rules:** + +- Use specific file paths: `git add src/auth/login.ts src/auth/login.test.ts` +- **Never** use `git add .`, `git add -A`, or `git add *` +- **Never stage:** + - `.env` or any secrets/env files + - `bun.lock` (only if commit is specifically a deps update) + - `worker-configuration.d.ts` (generated file) + - Any file matching `.gitignore` patterns + +**Commit using HEREDOC** to preserve multi-line formatting: + +```bash +git commit -m "$(cat <<'EOF' +✨ feat(scope): subject + +- Bullet one +- Bullet two + +Closes: #123 +EOF +)" +``` + +After committing, run `git log --oneline -3` and show the result. + +### Step 6: Multi-commit workflow + +When splitting into multiple commits: + +1. `git restore --staged .` — unstage everything +2. Commit in this order: deps first → refactors → features → fixes → docs +3. For each group: + - `git add ` + - `git diff --cached --stat` — verify what's staged + - Commit with its own message +4. Each commit must leave the codebase in a working state + +For partial file staging (one file has changes for different commits): + +```bash +git add -p src/api/handler.ts # stage only relevant hunks +git commit -m "🐛 fix(api): validate request body" + +git add -p src/api/handler.ts # stage remaining hunks +git commit -m "✨ feat(api): add rate limiting headers" +``` + +--- + +## Mode 2 — Setup + +When user asks about setting up git workflow tooling, read the appropriate +reference file: + +- **`references/hooks-and-tools.md`** — lefthook, commitlint, cz-git setup + and configuration. Read when user asks to setup hooks, configure commit + linting, or initialize git workflow for a new project. + +- **`references/security.md`** — gitleaks configuration, secret scanning, + what to do when secrets are found, commit signing, CODEOWNERS. + Read when user asks about security scanning or secret management. + +- **`references/commit-conventions.md`** — detailed rules on types, scopes, + breaking changes, body/footer format, good/bad examples. + Read when user asks about commit message conventions or wants examples. + +- **`references/commit-workflow.md`** — manual staging strategies, hunk-level + staging with `git add -p`, interactive workflow without Claude Code. + Read when user asks about the manual commit process. diff --git a/home/dot_claude/skills/git-workflow/evals/evals.json b/home/dot_claude/skills/git-workflow/evals/evals.json new file mode 100644 index 0000000..2838c61 --- /dev/null +++ b/home/dot_claude/skills/git-workflow/evals/evals.json @@ -0,0 +1,75 @@ +{ + "skill_name": "git-workflow", + "evals": [ + { + "id": 1, + "prompt": "commit these changes", + "expected_output": "Analyzes diff, proposes gitmoji commit message, asks for confirmation before staging", + "expectations": [ + "Runs git status and git diff before proposing anything", + "Proposes a commit message using the emoji type(scope): subject format", + "Asks for confirmation (yes / edit / cancel) before staging files", + "Does not run git add . or git add -A", + "Does not commit without user confirmation" + ] + }, + { + "id": 2, + "prompt": "I changed 8 files across auth and billing, commit everything as one commit", + "expected_output": "Identifies mixed scopes, proposes splitting into 2+ commits, does not blindly commit everything together", + "expectations": [ + "Detects that files span multiple unrelated scopes (auth and billing)", + "Recommends splitting into at least 2 commits rather than one", + "Proposes separate commit messages for each group", + "Explains the reason for splitting (mixed scope)", + "Does not proceed with a single combined commit without flagging the issue" + ] + }, + { + "id": 3, + "prompt": "setup lefthook and commitlint for my new project", + "expected_output": "Reads hooks-and-tools.md and produces lefthook.yml, commitlint.config.mjs, and install commands", + "expectations": [ + "Reads the hooks-and-tools.md reference file before responding", + "Provides a lefthook.yml config covering pre-commit, commit-msg, and pre-push hooks", + "Provides a commitlint.config.mjs with the emoji type list", + "Includes the install command sequence (bun add, lefthook install)", + "Does not invent config from memory without referencing the file" + ] + }, + { + "id": 4, + "prompt": "I accidentally committed an API key in the last commit, what do I do", + "expected_output": "Reads security.md, guides through key rotation first then history cleanup", + "expectations": [ + "Reads the security.md reference file before responding", + "Instructs to rotate/revoke the key immediately as the first step", + "Explains how to remove the secret from git history (BFG or git-filter-repo)", + "Mentions that force push is required and warns about coordination with collaborators", + "Does not suggest git commit --amend as a safe fix (amend only rewrites local unpushed history)" + ] + }, + { + "id": 5, + "prompt": "I have one file with two unrelated fixes — a CSS typo and a JS bug. How should I commit this?", + "expected_output": "Recommends git add -p to stage hunks separately, proposes two commit messages", + "expectations": [ + "Recommends using git add -p (patch mode) for partial file staging", + "Proposes two separate commit messages with appropriate types (style + fix)", + "Does not suggest committing the file as a whole in one commit", + "Shows the correct git add -p command syntax" + ] + }, + { + "id": 6, + "prompt": "check if my branch has any secrets before I push", + "expected_output": "Reads security.md, provides gitleaks command and manual grep pattern for staged/history scanning", + "expectations": [ + "Reads the security.md reference file", + "Provides a gitleaks command appropriate for scanning (git history or staged)", + "Includes the manual grep pattern as a fallback", + "Clarifies the difference between scanning staged-only vs full history" + ] + } + ] +} diff --git a/home/dot_claude/skills/git-workflow/references/commit-conventions.md b/home/dot_claude/skills/git-workflow/references/commit-conventions.md new file mode 100644 index 0000000..5ea21c5 --- /dev/null +++ b/home/dot_claude/skills/git-workflow/references/commit-conventions.md @@ -0,0 +1,203 @@ +# Commit Conventions Reference + +Complete rules for Conventional Commits with gitmoji in this project. + +--- + +## Message Format + +``` +emoji type(scope): subject + +[optional body] + +[optional footer(s)] +``` + +- Header line: mandatory, ≤100 characters total (including emoji) +- Body: separated from header by one blank line, wrap at 100 chars +- Footer: separated from body by one blank line + +--- + +## Types (Priority Order) + +When a commit includes multiple concerns, pick the highest-priority type. + +| # | Type | Emoji | Description | Example | +| -- | ------------- | ----- | ------------------------------- | ---------------------------------------------------- | +| 1 | `init` | 🎉 | Project kickoff | `🎉 init(all): scaffold project structure` | +| 2 | `feat` | ✨ | New user-facing feature | `✨ feat(cart): add quantity selector` | +| 3 | `fix` | 🐛 | Bug fix | `🐛 fix(auth): prevent session timeout on refresh` | +| 4 | `hotfix` | 🚑️ | Critical production fix | `🚑️ hotfix(api): patch null pointer in payment flow` | +| 5 | `refactor` | ♻️ | Restructure, no behavior change | `♻️ refactor: extract validation into shared utils` | +| 6 | `perf` | ⚡️ | Performance improvement | `⚡️ perf(db): add index on users.email` | +| 7 | `test` | ✅ | Tests only | `✅ test(api): add integration tests for /orders` | +| 8 | `docs` | 📝 | Documentation only | `📝 docs: update API authentication guide` | +| 9 | `update-deps` | ⬆️ | Dependency upgrades | `⬆️ update-deps: upgrade react from 18.2 to 19.0` | +| 10 | `configs` | 🔧 | Config file changes | `🔧 configs(biome): enable sorted imports rule` | +| 11 | `chore` | 🔨 | Maintenance, tooling | `🔨 chore: clean up unused scripts` | +| 12 | `style` | 💄 | CSS/visual, formatting only | `💄 style: fix indentation in config files` | +| 13 | `breaking` | 💥 | Breaking changes | `💥 breaking(api): require auth for all endpoints` | +| 14 | `deploy` | 🚀 | Deployment | `🚀 deploy(cloudflare): deploy production` | + +--- + +## Scope + +The scope narrows down which area of the codebase was affected. + +**Rules:** + +- Lowercase, single word or hyphenated +- Derive from directory/module names: `src/auth/` → `auth` +- Keep consistent — don't alternate between `auth` and `authentication` +- Omit when changes span many unrelated areas + +**Common scopes:** `api`, `auth`, `ui`, `db`, `cli`, `config`, `deps`, `core`, +`resume`, `seo`, `cf`, `pwa`, `rss`, `css`, `content` + +**When to omit:** + +- Change touches many modules equally: `♻️ refactor: rename userId to id` +- Scope adds no useful info: `🔨 chore: update lockfile` + +--- + +## Subject + +The text after `type(scope):` — this is what people read in `git log --oneline`. + +1. **Imperative present tense** — "add", "fix", "remove" (not "added", "fixes") +2. **Lowercase first letter** — `fix: correct null check` not `fix: Correct null check` +3. **No period** at the end +4. **Describe the what** from a user/system perspective + +Verification trick: your subject should complete "If applied, this commit will ___." + +``` +✨ feat(search): add autocomplete suggestions +→ "this commit will add autocomplete suggestions" ✓ + +✨ feat(search): added autocomplete suggestions +→ "this commit will added autocomplete suggestions" ✗ +``` + +--- + +## Body + +Include when the subject alone doesn't explain **why** the change exists. + +**When to include:** + +- Non-obvious bug fix (explain root cause) +- Trade-offs or alternatives considered +- Workaround with known limitations +- Context others need for a migration or refactor + +**Format:** + +- Each bullet starts with a verb: Add, Fix, Remove, Update, Refactor, Extract +- One fact per bullet — no filler +- Explain why, not what (the diff shows what) + +``` +🐛 fix(payments): retry failed charges with exponential backoff + +- Stripe webhook delivery was unreliable during high traffic +- Charges marked as failed were not retried, causing lost revenue +- Add backoff (1s, 2s, 4s, 8s, max 5 retries) to handle transient errors +``` + +--- + +## Footer + +**Issue/ticket references:** + +- `Closes: #123` — GitHub issue (auto-closes on merge) +- `Fixes: #456` — same behavior as Closes +- `Refs: ELWB-1234` — external ticket reference (Jira, Linear, etc.) +- Multiple issues: one per line + +``` +✨ feat(auth): add refresh token rotation + +- Implement automatic token refresh on 401 response +- Store refresh token in httpOnly cookie + +Closes: #891 +Refs: ELWB-1234 +``` + +**Only include when:** + +- User explicitly mentions a ticket/issue number +- The commit clearly resolves a tracked issue + +--- + +## Breaking Changes + +Two ways to mark: + +1. Add `!` after type/scope: + +``` +💥 breaking(api)!: require authentication for all endpoints +``` + +2. Add `BREAKING CHANGE:` footer: + +``` +💥 breaking(api): require authentication for all endpoints + +BREAKING CHANGE: all endpoints now require Bearer token. +Unauthenticated requests return 401. +``` + +--- + +## Special Rules + +**AI attribution:** Never include. No "Generated by", "Co-Authored-By: AI", +or any AI tool credit in the commit message. + +**.claude directory:** + +- Commits only touching `.claude/` → `📝 docs(.claude): description` +- When `.claude/` changes accompany code → use the code change's type/scope, + don't split `.claude/` into its own commit + +--- + +## Good Examples + +``` +✨ feat(search): add autocomplete suggestions dropdown + +⬆️ update-deps: upgrade react from 18.2 to 19.0 + +🐛 fix(api): prevent duplicate webhook deliveries + +- The dispatcher retried on HTTP 202, treating them as failures +- Now only 4xx/5xx responses trigger retries + +Closes: #342 + +♻️ refactor: extract validation logic into shared utility module + +- Five controllers duplicated email and phone validation +- Centralize into src/utils/validators.ts + +🚀 deploy(cloudflare): deploy v2.1.0 to production +``` + +## Bad Examples + +``` +feat: Changes ← vague, no emoji, capitalized +fixed the login bug. ← no type/emoji, past tense, period +✨ feat(auth): Add OAuth2 and fix billing ← mixed concerns, capitalized +``` diff --git a/home/dot_claude/skills/git-workflow/references/commit-workflow.md b/home/dot_claude/skills/git-workflow/references/commit-workflow.md new file mode 100644 index 0000000..f0cc60d --- /dev/null +++ b/home/dot_claude/skills/git-workflow/references/commit-workflow.md @@ -0,0 +1,163 @@ +# Commit Workflow Reference + +Step-by-step manual process for staging, analyzing, splitting, and committing. +This is the reference for when working without Claude Code automation. + +--- + +## Stage Intentionally + +Never blindly stage everything. + +```bash +git add src/auth/login.ts src/auth/login.test.ts # specific files +git add -p src/api/handler.ts # individual hunks +``` + +Interactive hunk prompts: `y` = stage, `n` = skip, `s` = split smaller, `q` = quit. + +**Inspect staged changes:** + +```bash +git diff --cached # full diff of staged +git diff --cached --stat # summary: files, insertions, deletions +git diff --cached --name-only # just file paths +git diff --cached --shortstat # one-line: 3 files, +42, -17 +``` + +**Unstage:** + +```bash +git restore --staged # unstage one file +git restore --staged . # unstage everything +``` + +--- + +## Split Decision + +| Single commit | Split into multiple | +| ----------------------------------- | ------------------------------------- | +| Same type AND scope | Mixed types (feat + fix) | +| ≤3 files | Mixed scopes (auth + billing) | +| ≤50 lines total | >10 files across unrelated areas | +| Splitting breaks intermediate state | Deps mixed with code changes | +| | Formatting mixed with logic changes | +| | Migration mixed with application code | + +### Grouping example + +``` +Staged diff: + M src/auth/login.ts (feat — new OAuth flow) + M src/auth/login.test.ts (test — tests for OAuth) + M src/billing/invoice.ts (fix — tax calculation) + M package.json (chore — add oauth library) + M bun.lock (chore — lockfile) + +Split into 3 commits: + 1. package.json + bun.lock → ⬆️ update-deps: add oauth2 client library + 2. src/auth/login.ts + test.ts → ✨ feat(auth): add OAuth2 PKCE flow + 3. src/billing/invoice.ts → 🐛 fix(billing): correct tax rate calculation +``` + +--- + +## Multi-Commit Execution + +```bash +git restore --staged . + +# --- Commit 1: deps --- +git add package.json bun.lock +git diff --cached --stat +git commit -m "⬆️ update-deps: add oauth2-client library" + +# --- Commit 2: feature --- +git add src/auth/login.ts src/auth/login.test.ts +git diff --cached --stat +git commit -m "$(cat <<'EOF' +✨ feat(auth): add OAuth2 PKCE flow + +- Implement authorization code flow with PKCE for mobile clients +- Use oauth2-client library for token exchange + +Closes: #214 +EOF +)" + +# --- Commit 3: bugfix --- +git add src/billing/invoice.ts +git commit -m "🐛 fix(billing): correct tax rate for EU customers" +``` + +### Partial file staging + +When one file contains changes for different commits: + +```bash +git add -p src/api/handler.ts # stage only relevant hunks +git commit -m "🐛 fix(api): validate request body before processing" + +git add -p src/api/handler.ts # stage remaining hunks +git commit -m "✨ feat(api): add rate limiting headers to response" +``` + +--- + +## Commit Message (manual) + +**Single-line:** + +```bash +git commit -m "✨ feat(scope): imperative description" +``` + +**Multi-line with HEREDOC:** + +```bash +git commit -m "$(cat <<'EOF' +✨ feat(auth): add session timeout configuration + +- Allow admins to configure session timeout per role +- Default remains 30 minutes, enterprise supports 24 hours +- Add timeout_minutes column to roles table + +Closes: #187 +EOF +)" +``` + +--- + +## Verify + +```bash +git log --oneline -5 # confirm commits in history +git show --stat HEAD # files and line counts in last commit +git status # working tree should be clean +``` + +### Amend (only before push) + +```bash +# Safe — not yet pushed +git commit --amend -m "🐛 fix(api): correct the error message format" + +# Already pushed — create a NEW commit instead +git commit -m "🐛 fix(api): update error message wording" +``` + +Never `git push --force` after amending shared history. + +--- + +## Common Mistakes + +| Mistake | Fix | +| --------------------------- | --------------------------------------------------- | +| `git add .` then commit | Stage specific files, review with `--stat` | +| Giant mixed commit | Split by type and scope | +| Vague message: "fix bug" | Be specific: "fix(cart): prevent negative quantity" | +| Past tense: "added feature" | Imperative: "add feature" | +| Amend after push | New commit instead | diff --git a/home/dot_claude/skills/git-workflow/references/hooks-and-tools.md b/home/dot_claude/skills/git-workflow/references/hooks-and-tools.md new file mode 100644 index 0000000..195a4b3 --- /dev/null +++ b/home/dot_claude/skills/git-workflow/references/hooks-and-tools.md @@ -0,0 +1,284 @@ +# Hooks & Tools Reference + +Setup guide for lefthook, commitlint, and cz-git. + +--- + +## Architecture Overview + +``` +pre-commit (fast, auto-fix) +├── biome check → lint staged files +├── biome format → format staged files +└── validate-branch → enforce branch naming + +commit-msg (validate message) +└── commitlint → enforce conventional commit format + +pre-push (slow, thorough) +├── type-check → tsc per workspace +├── biome lint → full project lint +└── gitleaks → secret scanning +``` + +The principle: fast checks that auto-fix go in pre-commit, message validation +in commit-msg, and slow thorough checks in pre-push. This keeps the commit +cycle fast while catching serious issues before code leaves your machine. + +--- + +## Lefthook + +Git hooks manager. Replaces husky with better performance and simpler config. + +**Install:** + +```bash +# macOS / Arch / most systems — install once, globally +brew install lefthook # macOS +sudo pacman -S lefthook # Arch + +# Init in repo (always needed, even with system install) +lefthook install +``` + +**Config** (`lefthook.yml`): + +```yaml +pre-commit: + parallel: true + commands: + biome-check: + glob: "*.{js,ts,jsx,tsx,mjs,cjs,astro,svelte,json,jsonc,css,md,mdx}" + run: bunx @biomejs/biome check --no-errors-on-unmatched --files-ignore-unknown=true {staged_files} + stage_fixed: true + biome-format: + glob: "*.{js,ts,jsx,tsx,mjs,cjs,astro,svelte,json,jsonc,css,md,mdx}" + run: bunx @biomejs/biome format --no-errors-on-unmatched --files-ignore-unknown=true --write {staged_files} + stage_fixed: true + +commit-msg: + commands: + commitlint: + run: bunx commitlint --edit {1} + +pre-push: + parallel: true + commands: + check-types-site: + run: "bun run --filter './apps/site' check:types" + check-types-api: + run: "bun run --filter './apps/api' check:types" + biome-lint: + run: bunx @biomejs/biome check . + gitleaks: + run: "gitleaks git --pre-commit --staged --verbose" +``` + +**Key options:** + +- `parallel: true` — run commands simultaneously (faster) +- `glob` — only run on matching file types +- `stage_fixed: true` — auto-stage files that were auto-fixed +- `{staged_files}` — lefthook variable, expands to staged file list +- `{1}` — the commit message file path (for commit-msg hooks) + +**Useful commands:** + +```bash +lefthook install # install hooks into .git/hooks/ +lefthook uninstall # remove hooks +lefthook run pre-commit # manually run a hook stage +``` + +**Skip hooks temporarily:** + +```bash +git commit --no-verify -m "🔨 chore: emergency fix" # skip pre-commit + commit-msg +git push --no-verify # skip pre-push +LEFTHOOK=0 git commit -m "..." # disable lefthook entirely +``` + +Use sparingly — hooks exist for good reason. + +--- + +## Commitlint + +Validates commit message format against conventional commit rules. + +**Install:** + +```bash +bun add -D @commitlint/cli +``` + +**Config** (`commitlint.config.mjs`): + +```javascript +import { defineConfig } from 'cz-git'; + +const types = [ + '🎉 init', + '✨ feat', + '🐛 fix', + '🚑️ hotfix', + '📝 docs', + '💄 style', + '♻️ refactor', + '⚡️ perf', + '✅ test', + '⬆️ update-deps', + '🔧 configs', + '🔨 chore', + '💥 breaking', + '🚀 deploy', +]; + +export default defineConfig({ + parserPreset: { + parserOpts: { + // Custom regex to parse emoji-prefixed types + headerPattern: /^(?.+?)(?:\((?.*)\))?!?:\s(?.+)$/, + headerCorrespondence: ['type', 'scope', 'subject'], + }, + }, + rules: { + 'body-leading-blank': [1, 'always'], + 'body-max-line-length': [2, 'always', 100], + 'footer-leading-blank': [1, 'always'], + 'footer-max-line-length': [2, 'always', 100], + 'header-max-length': [2, 'always', 100], + 'header-trim': [2, 'always'], + 'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']], + 'subject-empty': [2, 'never'], + 'subject-full-stop': [2, 'never', '.'], + 'type-case': [2, 'always', 'lower-case'], + 'type-empty': [2, 'never'], + 'type-enum': [2, 'always', types], + }, + prompt: { + // cz-git interactive prompt config (for manual use) + useEmoji: true, + emojiAlign: 'left', + types: [ + { value: 'init', name: 'init: 🎉 Begin a project.', emoji: '🎉' }, + { value: 'feat', name: 'feat: ✨ A new feature', emoji: '✨' }, + { value: 'fix', name: 'fix: 🐛 A bug fix', emoji: '🐛' }, + { value: 'hotfix', name: 'hotfix: 🚑️ Critical hotfix.', emoji: '🚑️' }, + { value: 'docs', name: 'docs: 📝 Documentation only changes', emoji: '📝' }, + { value: 'style', name: 'style: 💄 Visual/formatting changes', emoji: '💄' }, + { value: 'refactor', name: 'refactor: ♻️ Code restructure', emoji: '♻️' }, + { value: 'perf', name: 'perf: ⚡️ Performance improvement', emoji: '⚡️' }, + { value: 'test', name: 'test: ✅ Tests', emoji: '✅' }, + { value: 'update-deps', name: 'update-deps: ⬆️ Upgrade dependencies.', emoji: '⬆️' }, + { value: 'configs', name: 'configs: 🔧 Config files.', emoji: '🔧' }, + { value: 'chore', name: "chore: 🔨 Maintenance", emoji: '🔨' }, + { value: 'breaking', name: 'breaking-change: 💥 Breaking changes.', emoji: '💥' }, + { value: 'deploy', name: 'deploy: 🚀 Deploy stuff.', emoji: '🚀' }, + ], + }, +}); +``` + +**How it integrates:** + +- Lefthook calls `bunx commitlint --edit {1}` at `commit-msg` stage +- Commitlint parses the message using the custom `headerPattern` regex +- The regex handles emoji-prefixed types: `✨ feat(scope): subject` +- If validation fails, the commit is rejected + +**Rule reference:** + +| Rule | Level | Meaning | +| -------------------- | -------- | ------------------------------- | +| `[2, 'always', ...]` | Error | Commit rejected if violated | +| `[1, 'always', ...]` | Warning | Shows warning but allows commit | +| `[0, ...]` | Disabled | Rule not enforced | + +--- + +## cz-git + +Interactive commit prompt. Used for **manual commits** (not Claude Code). + +**Install:** + +```bash +bun add -D cz-git commitizen +``` + +**Add to package.json:** + +```json +{ + "config": { + "commitizen": { + "path": "node_modules/cz-git" + } + }, + "scripts": { + "cz": "cz" + } +} +``` + +**Usage:** + +```bash +bun run cz # interactive commit wizard +git cz # alternative (if installed globally) +``` + +cz-git reads the `prompt` section of `commitlint.config.mjs` for its UI. +This means commitlint and cz-git share the same config file — single source +of truth for types, emojis, and rules. + +**Aliases** (defined in config): + +```bash +# Quick commits using aliases +bun run cz --alias=b # → "🔨 chore: bump dependencies" +bun run cz --alias=c # → "🔧 configs: update config files" +bun run cz --alias=f # → "📝 docs: fix typos" +``` + +--- + +## Claude Code vs Manual Workflow + +| Aspect | Claude Code | Manual (cz-git) | +| --------------------- | ---------------------------- | ---------------------------- | +| **Analyze diff** | Automatic | You read the diff yourself | +| **Choose type/scope** | Claude proposes | cz-git prompts you | +| **Write message** | Claude drafts, you confirm | You type it | +| **Validation** | Lefthook + commitlint (same) | Lefthook + commitlint (same) | +| **Split commits** | Claude suggests grouping | You decide manually | + +Both paths go through the same lefthook hooks — validation is identical. + +--- + +## New Project Setup Checklist + +```bash +# 1. Install JS packages (lefthook and gitleaks are system-wide binaries, not devDeps) +bun add -D @commitlint/cli cz-git commitizen + +# 2. Install lefthook hooks into .git/hooks/ +lefthook install + +# 3. Create config files +# - commitlint.config.mjs (see template above) +# - lefthook.yml (see template above) +# - .gitleaks.toml (optional, for allowlist rules) + +# 4. Add to package.json scripts + commitizen config +# "cz": "cz" +# "config": { "commitizen": { "path": "node_modules/cz-git" } } + +# 5. Verify +lefthook run pre-commit # test pre-commit hooks manually +echo "test" | bunx commitlint # test commitlint rejects bad messages +gitleaks detect --source . # test gitleaks secret scan +``` diff --git a/home/dot_claude/skills/git-workflow/references/security.md b/home/dot_claude/skills/git-workflow/references/security.md new file mode 100644 index 0000000..b8cd998 --- /dev/null +++ b/home/dot_claude/skills/git-workflow/references/security.md @@ -0,0 +1,227 @@ +# Security Reference + +Secret scanning, response procedures, commit signing, and access control. + +--- + +## Quick Scan (Manual) + +Run on staged changes before committing: + +```bash +git diff --cached -U0 | grep -nE \ + 'AKIA[A-Z0-9]{16}|-----BEGIN .* PRIVATE KEY-----|xox[bpors]-|[sr]k_(live|test)_|ghp_[A-Za-z0-9]{36}|eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+|AIza[0-9A-Za-z_-]{35}|mongodb\+srv://[^[:space:]]+|postgres://[^[:space:]]+@' \ + && echo "SECRETS DETECTED — DO NOT COMMIT" || echo "Clean" +``` + +## Secret Patterns + +| Category | Pattern | Example | +| -------------- | -------------------------------- | ---------------------------- | +| AWS Access Key | `AKIA[A-Z0-9]{16}` | `AKIAIOSFODNN7EXAMPLE` | +| Private Key | `-----BEGIN .* PRIVATE KEY-----` | PEM files | +| GitHub Token | `ghp_`, `gho_`, `github_pat_` | `ghp_abc123def456...` | +| Slack Token | `xox[bpors]-` | `xoxb-1234567890-abc` | +| Stripe Key | `[sr]k_(live | test)_` | +| Google API Key | `AIza` + 35 chars | `AIzaSyA1b2c3d4...` | +| JWT | `eyJ...\.eyJ...` | `eyJhbGciOiJIUzI1NiJ9...` | +| Database URL | `protocol://user:pass@host` | `postgres://admin:s3cret@db` | + +--- + +## Gitleaks + +Gitleaks runs automatically at **pre-push** via lefthook. It scans the entire +git history, not just staged files. + +**Manual run:** + +```bash +gitleaks git --verbose # scan full history +gitleaks git --pre-commit --staged # scan staged only +gitleaks detect --source . # scan working directory +``` + +**Configuration** (`.gitleaks.toml`): + +```toml +[allowlist] +description = "Global allowlist" +paths = [ + '''vendor/''', + '''node_modules/''', + '''\.test\.''', +] + +[[rules]] +id = "custom-api-key" +description = "Custom API key pattern" +regex = '''MYAPP_KEY_[A-Za-z0-9]{32}''' +``` + +**Adding exceptions:** + +```toml +# Allow specific strings (false positives) +[allowlist] +commits = ["abc123def456"] # specific commit hash +regexes = ['''EXAMPLE_KEY_[A-Z]+'''] # pattern to allow + +# Inline suppression (in source code) +# gitleaks:allow +const EXAMPLE_KEY = "not-a-real-key" # gitleaks:allow +``` + +--- + +## Response: Secrets Found BEFORE Push + +The secret hasn't left your machine. You have time. + +```bash +# 1. STOP — do not commit or push + +# 2. Unstage the file +git restore --staged + +# 3. Remove secret from source — use env var instead +# const key = process.env.STRIPE_SECRET_KEY + +# 4. Add to .gitignore if the file should never be tracked +echo ".env.local" >> .gitignore + +# 5. Stage the cleaned version +git add .gitignore + +# 6. Rotate the secret — treat it as compromised +# Even if never pushed, it existed in a diff on disk +``` + +## Response: Secrets Found AFTER Push + +The secret is compromised. Rotate first, clean history second. + +```bash +# 1. ROTATE THE SECRET IMMEDIATELY +# Generate new credentials in AWS/GitHub/Stripe console +# Update all services using the old secret + +# 2. Remove from history — BFG Repo-Cleaner (preferred) +brew install bfg +git clone --mirror git@github.com:org/repo.git +bfg --replace-text passwords.txt repo.git # one secret per line +cd repo.git +git reflog expire --expire=now --all +git gc --prune=now --aggressive +git push --force + +# 3. Alternative — git-filter-repo +pip install git-filter-repo +git filter-repo --blob-callback ' + return blob.data.replace(b"sk_live_abc123", b"REDACTED") +' +git push --force origin --all +``` + +Force push rewrites history for all collaborators — coordinate with team. + +--- + +## Files to Always Ignore + +```gitignore +# Secrets and environment +.env +.env.* +!.env.example +.envrc + +# Keys and certificates +*.pem +*.key +*.p12 +*.pfx +id_rsa +id_ed25519 + +# Credential files +credentials.json +*secret*.json +service-account*.json +.npmrc +.pypirc + +# OS +.DS_Store +Thumbs.db + +# Dependencies +node_modules/ +.venv/ +__pycache__/ + +# Build output +dist/ +build/ +``` + +--- + +## Commit Signing + +### SSH signing (recommended — simpler) + +```bash +git config --global gpg.format ssh +git config --global user.signingkey ~/.ssh/id_ed25519.pub +git config --global commit.gpgsign true +``` + +Add the same SSH public key to GitHub: **Settings > SSH and GPG keys** → key type "Signing Key". + +### GPG signing + +```bash +gpg --full-generate-key # RSA 4096 +gpg --list-secret-keys --keyid-format=long # copy key ID +gpg --armor --export # add to GitHub + +git config --global user.signingkey +git config --global commit.gpgsign true +``` + +Verify: `git log --show-signature -1` + +--- + +## CODEOWNERS + +Protect sensitive paths by requiring review from specific teams. + +``` +# .github/CODEOWNERS +/.env.example @org/security +/.github/workflows/ @org/devops +/src/auth/ @org/security @org/backend +/src/payments/ @org/billing @org/security +/migrations/ @org/backend-leads +``` + +Enable: Repository Settings > Branch protection > Require review from Code Owners. + +--- + +## GitHub Secret Scanning + +``` +Repository Settings > Code security and analysis > Secret scanning > Enable +Repository Settings > Code security and analysis > Push protection > Enable +``` + +False positive config (`.github/secret_scanning.yml`): + +```yaml +paths-ignore: + - "docs/**" + - "tests/fixtures/**" +``` diff --git a/home/dot_claude/statusline-command.sh b/home/dot_claude/statusline-command.sh new file mode 100644 index 0000000..483b202 --- /dev/null +++ b/home/dot_claude/statusline-command.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Claude Code status line +# Segments: cwd | branch | model [effort] | ctx X% | 5h: X% | 7d: X% + +input=$(cat) + +# jq with PATH fallback (GUI launches don't inherit shell PATH) +JQ=$(command -v jq || true) +for cand in /opt/homebrew/bin/jq /usr/local/bin/jq /usr/bin/jq; do + [ -n "$JQ" ] && break + [ -x "$cand" ] && JQ="$cand" +done +[ -z "$JQ" ] && { + printf '%b\n' '\033[31mstatusline: jq not found\033[0m' + exit 0 +} + +cwd=$(echo "$input" | "$JQ" -r '.cwd // empty') +model=$(echo "$input" | "$JQ" -r '.model.display_name // empty') +effort=$(echo "$input" | "$JQ" -r '.effort.level // empty') +remaining=$(echo "$input" | "$JQ" -r '.context_window.remaining_percentage // empty') +five_pct=$(echo "$input" | "$JQ" -r '.rate_limits.five_hour.used_percentage // empty') +week_pct=$(echo "$input" | "$JQ" -r '.rate_limits.seven_day.used_percentage // empty') + +branch="" +[ -n "$cwd" ] && branch=$(git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null) + +# Shorten cwd starship-style: $HOME → ~, then keep only the last N segments +# (prefixed with …/). Override the count with CLAUDE_STATUSLINE_CWD_SEGMENTS. +short_cwd() { + local p="$1" max="${CLAUDE_STATUSLINE_CWD_SEGMENTS:-3}" + [ "$p" = "$HOME" ] && { + printf '~' + return + } + # SC2088: the ~ here is a literal display prefix, not a path to expand. + # shellcheck disable=SC2088 + case "$p" in "$HOME"/*) p="~/${p#"$HOME"/}" ;; esac + + local IFS='/' seg parts=() + for seg in $p; do [ -n "$seg" ] && parts+=("$seg"); done + local n=${#parts[@]} + if [ "$n" -le "$max" ]; then + printf '%s' "$p" + else + local out="" i + for ((i = n - max; i < n; i++)); do out="$out/${parts[i]}"; done + printf '…%s' "$out" + fi +} +cwd=$(short_cwd "$cwd") + +dim='\033[2m' +bright='\033[1m' +yellow='\033[33m' +red='\033[31m' +green='\033[32m' +cyan='\033[36m' +reset='\033[0m' +sep="${dim} | ${reset}" + +# Account indicator — driven by $CLAUDE_CONFIG_DIR, exported by the claude / +# claude-work fish wrappers (dotfiles docs/claude-code.md). Empty ⇒ personal. +case "${CLAUDE_CONFIG_DIR%/}" in +*/.claude.work) + acct="work" + acct_c="${red}" + ;; +*) + acct="personal" + acct_c="${green}" + ;; +esac + +line="${bright}${acct_c}${acct}${reset}${sep}${dim}${cwd}${reset}" +[ -n "$branch" ] && line="${line}${sep}${bright}${cyan}${branch}${reset}" + +if [ -n "$model" ]; then + seg="${dim}${model}${reset}" + [ -n "$effort" ] && [ "$effort" != "medium" ] && seg="${seg} ${dim}[${effort}]${reset}" + line="${line}${sep}${seg}" +fi + +if [ -n "$remaining" ]; then + r=$(printf '%.0f' "$remaining") + if [ "$r" -gt 80 ]; then + c="${green}" + elif [ "$r" -gt 70 ]; then + c="${yellow}" + else c="${red}"; fi + line="${line}${sep}${c}ctx: ${r}%${reset}" +fi + +if [ -n "$five_pct" ]; then + u=$(printf '%.0f' "$five_pct") + [ "$u" -ge 70 ] && c="${bright}${yellow}" || c="${dim}" + line="${line}${sep}${c}5h: ${u}%${reset}" +fi + +if [ -n "$week_pct" ]; then + w=$(printf '%.0f' "$week_pct") + [ "$w" -ge 70 ] && c="${bright}${yellow}" || c="${dim}" + line="${line}${sep}${c}7d: ${w}%${reset}" +fi + +printf '%b\n' "$line"