Welcome to the dotfiles repository. This file serves as the entry point for agent-based discovery of the tools, configurations, and workflows contained within this repo. It is read by both Gemini CLI (as GEMINI.md) and Claude Code (as CLAUDE.md — a symlink to this file).
opt/bin/: A collection of utility scripts and binaries. See opt/bin/GEMINI.md for a categorized registry of these tools.opt/profiles/: Shell configuration files (.zshrc, .bashrc, .tmux.conf, etc.). See opt/profiles/GEMINI.md for details.opt/docs/: Legacy and reference documentation for various tools and setups. See opt/docs/GEMINI.md.src/: Non-Go custom tools and agent skills (shell/skill tooling). Go code no longer lives here — all Go modules live undersdk/. See src/GEMINI.md.sdk/: Go modules (gss,gsl,wol,tmux-mgr), each independentlygo install-able asgithub.com/sfc-gh-eraigosa/dotfiles/sdk/<tool>. All Go code lives here, not undersrc/. (Cutover in progress — see docs/mbo/plans/2026-06-04-sdk-migration-plan.md.)opt/Desktop/Apps/scripts/: Windows-side automation deployed to the Desktop (macOS-style hotkeys + Wispr Flow voice dictation inmacos.ahk, PowerToys/app/font setup). See opt/Desktop/Apps/scripts/GEMINI.md for the inventory, the WSL→Windows dev loop, and AutoHotkey v2 gotchas.archive/: Retired-but-kept artifacts (not wired into install). See archive/GEMINI.md for the inventory and restore instructions.ai/hooks/: Unified agent hooks (safety, privacy) shared across CLIs.ai/skills/: Shared agent skills (each aSKILL.mdfolder) linked into Claude + Gemini bysync-skills; each should ship anevals/evals.jsonvalidated bymake skill-evals. See ai/skills/GEMINI.md.ai/gemini/: Gemini-specific commands, TOML policies, and settings.ai/claude/: Claude-specific commands, settings, and hook templates.ai/plugins.yaml: Declarative manifest of the Claude Code plugins this repo installs/enables (ensure-only viasync-plugins). See docs/ai-plugins.md for the plugin summary, first-usage examples, and the Gemini-extension path.ai/teams/: Specialized agent teams installed as native subagents for Claude, Gemini, Antigravity, and Ollama. Personas declare an abstracttier:resolved viamodel-map.yaml;install_ai_teams.shemits each tool's format and/teamroutes tasks to the right team/member. See ai/teams/GEMINI.md.docs/: Repository documentation. Objective-driven design work — issues,gssdraft PRs, new features/skills/CLIs/services — starts indocs/mbo/: the Management-By-Objectivedesign → spec → planpipeline, per-task skill routing, and the objective tracker (docs/mbo/index.md). See docs/GEMINI.md.
- Tool Discovery: Check
opt/scripts/GEMINI.mdfor available shell scripts. - Configuration: Shell profiles and aliases are maintained in
opt/profiles/. - Progressive Loading: Only read subdirectory
GEMINI.md(orCLAUDE.md— same file) when specifically needing information about that section, to conserve context. - GEMINI.md + CLAUDE.md in every documented directory: Whenever a new directory is added that contains tools, scripts, or documentation worth describing to AI agents, create a
GEMINI.mdin that directory and aCLAUDE.md -> GEMINI.mdsymlink alongside it (ln -s GEMINI.md CLAUDE.md). This ensures both Gemini CLI and Claude Code can navigate the repo from any subdirectory. The symlink keeps both agents in sync from a single source file. Add a link to the newGEMINI.mdin this root file's Repository Structure section. - Skills are shared: any
SKILL.mdundersrc/(e.g.src/ssh-host-finder/SKILL.md,src/wispr-flow-debug/SKILL.md, or a tool'ssrc/<tool>/skill/) drives both assistants —sync-skillsdiscovers everySKILL.mdand links it into~/.claude/skillsand~/.agents/skills. Edit once, benefit twice. - Skills should ship evals: a skill folder should carry an
evals/evals.json(theskill-creatorformat —{ "skill_name", "evals": [ { "id", "prompt", "expected_output" } ] }) capturing its trigger/behavior cases.make skill-evals(→opt/scripts/system/skill-eval.sh --check) deterministically validates every skill's corpus and reports a SKIP for any skill folder that has none — no model calls, CI-safe. Behavioral grading (with-skill vs baseline accuracy) is the on-demandskill-creatorloop, not this gate. Seeai/skills/GEMINI.md.
- Surface every must-answer question through the interactive prompt, not plain prose. Whenever you need a decision from the user before you can proceed — a genuine fork you can't resolve from the request, the code, or a sensible default — present it via the assistant's interactive question tool so it renders as a distinct, colorized prompt that's impossible to miss in a wall of output. In Claude Code that is the
AskUserQuestiontool; in Gemini CLI / other harnesses use the equivalent interactive confirmation/elicitation tool. - Give real options. List 2–4 concrete, mutually-exclusive choices; put the recommended one first and label it
(Recommended). The user can always pick "Other". - Graceful fallback when no interactive tool exists (e.g. a headless or pipe-driven run): format the question as a visually distinct block so it still stands out — a blockquote led by a bold marker, e.g.
> ⚠️ **NEEDS YOUR INPUT:** …— never a sentence buried mid-paragraph. - Don't overuse it. Skip the prompt for trivial choices with an obvious default or facts you can verify yourself — pick the sensible option, state it in passing, and proceed. Reserve the colorized prompt for decisions whose answer actually changes what you do next.
- This generalizes the mandatory-confirmation rule under Git Workflow below (which already routes
git add/commit/gss push/gss prthroughAskUserQuestion): that gate is one instance of this broader convention.
- Shell Portability Standard (read before writing any
.sh): All shell scripts and sourced profile fragments MUST follow docs/mbo/specs/shell-portability.md — the normative contract for working identically across WSL2-Ubuntu, macOS (zsh + BSD coreutils + bash 3.2), and Linux (Raspberry Pi / Jetson Nano). It covers shebang policy, banned zsh-isms (read -A), BSD-vs-GNU coreutil traps (sed -i,stat,date, …), the mandatoryeval "$(tool init)"PATH-clobber guard (the bug that broke macOSinstall.sh), and a per-script checklist. CI enforces the mechanical parts viamake lint-shelland theshell-lintworkflow; review enforces the rest. - Cross-shell portability gate is ENFORCING — run
make lint-portabilitybefore pushing any shell change.opt/scripts/system/shell-portability-scan.sh --strictruns in theshell-lintworkflow and fails CI on any Tier 1 (dash/bin/shparse breakage — the class that caused the Raspberry Pi GUI login loop) or Tier 2 (macOS BSD-coreutil / bash-3.2 hazard) finding. It catches whatshellcheckandbash -nmiss (both pass bashisms). When writing or editing a script, conform to these rules so you don't trip the gate:- Shebang: executable scripts use
#!/usr/bin/env bash(never#!/bin/bash— that's the macOS 3.2 relic; never#!/bin/shunless the script is genuinely POSIX). Sourced fragments use# shellcheck shell=bash. - Anything dash sources at login is POSIX-only:
~/.profile,~/.xsessionrc, and every file they unconditionally dot-source (e.g.~/.nano_profile). Usename() { … }(neverfunction name { … }), no[[ … ]], no arrays — a parse error here aborts the LightDM Xsession and bounces you to the greeter. - Use portable tools:
command -vnotwhich;grep -Enotgrep -P/\K;cd -- "$(dirname "$0")" && pwd -Pnotreadlink -f;sed -i.bak … && rm -f ….baknot baresed -i; probestat -fthenstat -c. - Bash-4 features (
declare -A,mapfile,${v,,}) break macOS system bash 3.2 — rewrite them, or require bash 4+ via#!/usr/bin/env bash(picks up Homebrew bash 5 on macOS) and document it. - Opt-out for a reviewed exception: add a trailing
# portability-ok: <reason>comment on the line and the scanner skips it. Use sparingly and always with a reason.
- Shebang: executable scripts use
- Use $HOME: Always use
${HOME}or~instead of absolute home paths (e.g.,/home/wenlockor/Users/eraigosa) in scripts, aliases, and configuration files to ensure they are portable across different systems and users. - Avoid Hardcoded Usernames: Never hardcode usernames in paths or instructions; use environment variables like
$USERif needed. - Avoid Hardcoded Paths: Use relative paths or environment variables (like
BASE_DIRininstall.sh) whenever possible.
- Minimal alias surface: prefer ONE canonical alias per workflow. Don't propose variants (
foo-c,foo-r,foo-status) up front — add them only when asked. The shorter the alias surface, the easier the dotfiles are to memorize and audit. - Self-contained shell config:
.bashrc/.zshrc/ sourced fragments must resolve paths from their own location (or$HOME/$DOTFILES_DIRenv vars), never from hardcoded$HOME/git/dotfiles. The repo can be cloned anywhere and the config must still work. - One install path: when adding shell config, wire it through
install.shso a fresh clone bootstraps cleanly. Don't rely on the user manually symlinking anything you create. - Worktree safety &
install.sh(critical): NEVER runinstall.shfrom agss featureworktree. The script creates absolute symlinks in your$HOME(e.g.,~/.zshrc -> .../dotfiles/opt/profiles/zshrc). Running it from a worktree will link your global configuration to a transient, task-specific path. Always switch to the main repository (~/git/dotfiles), checkout the desired branch, and runinstall.shfrom there to ensure your system remains in a predictable, stable state. install.shis interactive — never run it non-interactively or backgrounded: on Windows/WSL it deploysopt/Desktop/*and then prompts ("Windows Desktop Customization detected … [y/n/s]") before running the PowerShell setup (Terminal themes, winget apps, the elevated AHK autostart task). With no TTY (a backgrounded run, or piping throughtail/redirection) that prompt blocks forever or is silently skipped, so the customization step never runs. Run it in a real terminal and answer the prompt. The earlier file deploy (e.g. the fixedmacos.ahk) happens before the prompt, so a skipped prompt still deploys files — it only skips the PowerShell setup/AHK reload.- AI-tool config provisioning — copy into well-known
$HOMEpaths; no new symlinks: There are two ways the repo populates$HOMEto configure the AI tools we support (~/.claude,~/.gemini, etc.): (1) copy a file into a well-known path under$HOME, or (2) a symlink into$HOMEthat referencesgit/dotfiles. Copy is the forward mechanism. Symlinks are legacy — still present and allowed, but do not introduce new ones; they are to be retired over time. Settings/config files must reference well-known$HOMEpaths (e.g.~/.claude/hooks/safety_guard.sh) and must not reference repo-internal paths like$HOME/git/dotfiles/ai/hooks/...(which couples global config to the checkout location and breaks on worktrees/CI). Host-local settings values are preserved by a forced-field merge on install (immutable fields like hook wiring are re-applied from the repo; undeclared host fields are left intact) — see docs/mbo/designs/2026-06-02-ai-config-home-provisioning.md.
- The git-safe-sync (gss) skill is the canonical commit + push path. Use the
/syncslash command for the common case (commit + push to main) — it scaffolds the introspect → propose → confirm → execute flow. - Confirmation is mandatory regardless of the user phrasing ("sync", "push", "commit it"): always present options via
AskUserQuestionbefore anygit add/git commit/gss push/gss pr. The gss skill's "Mandatory Confirmation" rule overrides any autonomous-mode preference. - Two-call recipe for gss push: the
safety_guard.shhook intentionally blocks chainedmkdir + token + gss pushin a single Bash call. Always issue the token-generation line as one Bash call andgss push(orgss pr/gss sync) as a separate second call. - Push the branch before creating a PR (no double prompt): a PR can only be opened once its branch exists on origin.
gss pushnow handles the first push of a brand-new branch — it detects a missingorigin/<branch>and creates it with--set-upstreaminstead of failing on the rebase step (the oldsync: … couldn't find remote ref <branch>error burned the approval token and forced a second confirmation). So the single token →gss pushrecipe works for a new branch; do not pre-push or regenerate the token. If you are on an older gss binary that still errors that way, fall back to one plaingit push -u origin HEAD(a plain push is not token-gated), verify withgit ls-remote --heads origin <branch>, then open the PR. - Group related changes: prefer one cohesive commit per logical unit. Stage files by explicit name (never
git add -A/git add .) to keep blast radius tight and avoid sweeping in unrelated dirty state. .gitignoreallowlist pattern (critical — read first): this repo's.gitignorestarts with*, which blocks every path by default. Files become visible to git only when an explicit!-rule opts them back in. The consequence is non-obvious and bites new contributors (human and agent): if you createsdk/gss/docs/plan.mdand don't see it ingit status, the file is not lost — it's been silently ignored by the default*because no!-rule covers its path yet. This is also why a freshly-created top-level folder (say~/git/dotfiles/notes/) won't show up ingit statusat all — git treats it as ignored, not as untracked, and skips it entirely.- Whenever you add a new file or directory: first check whether the path is covered by an existing
!-rule. Top-level paths already opted in include!src/**,!opt/**,!ai/**,!system/**,!docs/**,!.config/**,!.github/**,!.devcontainer/**,!scripts/**, plus per-file allowlists forREADME.md,CONTRIBUTING.md,Makefile,install.sh,LICENSE,Dockerfile,azure-pipelines.yml,**/GEMINI.md,**/CLAUDE.md. Anything outside those trees is invisible by default. - Verify, don't assume. After creating a new file, run
git status --short -- <path>andgit check-ignore -v <path>. If the file does not appear ingit status, add an!-rule to.gitignorebefore attempting to stage. Never paper over this withgit add -f— forced adds bypass the policy and leave the next contributor confused about whether the path is supposed to be tracked. - When in doubt, opt in explicitly. Prefer narrow rules (
!sdk/gss/**) over broad ones (!**/*.md). Document any path that is intentionally local-only with an inline.gitignorecomment explaining why (examples:opt/.DS_Store,ai/claude/settings.json,sdk/tmux-mgr/tmux-mgr— each has a comment line describing the reason). - Worked example for
sdk/gss/docs/: the design document is stage-able only because!src/**(lines 25–26 of.gitignore) opts in the entiresrc/tree. If we ever moved gss docs out ofsrc/(e.g. to a top-leveldesign/folder), the new path would be invisible until we added!design/and!design/**rules. Conversely, anything we add undersrc/is auto-tracked — no per-file rule needed. The same logic explains whysdk/gss/docs/design.mdshows ingit statustoday: it's covered by!src/**, not by a per-doc rule. - Never rely on "it's already covered by
*" as a substitute for a documented decision. A path is either explicitly tracked or explicitly local-only; ambiguity is a bug. - Note on
docs/at the repo root:!docs/and!docs/**are in.gitignoreso the top-leveldocs/tree, if/when it appears, is tracked. This is distinct fromsdk/gss/docs/(covered by!src/**) and fromopt/docs/(covered by!opt/**).
- Whenever you add a new file or directory: first check whether the path is covered by an existing
ai/hooks/safety_guard.shis a PreToolUse hook with regex-based deny rules. Its companion test driver isai/hooks/safety_guard_test.sh.- When editing the hook: extend
safety_guard_test.shfirst. Add at least one newassert_exit 0case proving a legitimate command of the same shape still passes, and one newassert_exit 2case proving the malicious shape is still blocked. Run the test driver and require all cases to pass before committing. - Beware bash regex line-spanning: bash regex
.*matches newlines and command separators (;,|,&). Use${SAFE_CHARS}(defined at the top of the hook as[^[:cntrl:];|&]) to scope a pattern to one shell-command segment. - Strip heredoc bodies before matching: multi-line content (commit messages, README text) gets passed via heredocs and routinely contains literal dangerous patterns. Use
strip_heredocs.awkto drop those bodies before regex evaluation.