Skip to content

Latest commit

 

History

History
211 lines (117 loc) · 50.2 KB

CONCEPTS.md

File metadata and controls

211 lines (117 loc) · 50.2 KB

Repository concepts

This document owns repository-specific design, lifecycle, and operator decisions that span files. It is not a file inventory or a replacement for the chezmoi reference. Source files define behavior; AGENTS.md owns contributor workflows and safety boundaries and routes each maintenance task here or to a narrower path rule or skill.

Bootstrap

Bootstrap remains interactive because it crosses account-specific input, system-level installation, and hardware-backed GPG keys. Scripts provision tools and machine state that file synchronization alone cannot express.

Scripts in .chezmoiscripts use before and after attributes to place ordering dependencies around file synchronization. Within a phase chezmoi runs them by target name, and the names carry no ordering prefix: a script that needs a sibling from the same phase checks for it before writing anything, so a name that sorts wrong fails at that check on a fresh machine instead of half-configuring it. The GPG script checks for the binaries the Brewfile installer provides, the glab setup script reads the envchain namespace the seeding script fills, and the coding-agent installer provides its own Node through proto install because reconcile-dev-tools sorts after it. run_once and run_onchange encode rerun behavior using the standard semantics in chezmoi's script guide.

Script identity includes rendered comments and other non-executable bytes. A comment-only edit to a run_once or run_onchange source can therefore schedule the script again, and a Brewfile comment changes the explicit hash embedded in its installer. A run_onchange script is keyed by its target name as well, so renaming one reschedules it with its body unchanged. Treat those comments as operational input and edit them only when the resulting rerun is in scope.

These dependencies shape the sequence:

  • Homebrew and Brewfile packages provide tools used by later scripts.
  • proto resolves every tool version from ~/.proto/.prototools, which chezmoi writes from dot_proto/dot_prototools during file synchronization. The dev-tools script installs those tools before reconciling the npm, uv, and Cargo declarations and therefore runs in the after phase: in the before phase that file does not exist yet, and proto refuses to install a tool whose required Node.js version is unconfigured.
  • claude and pi come from their vendor installers in run_once_after_install-agent-clis.sh, and codex comes from the codex cask declared in Brewfile. The script runs in the after phase because Pi needs the Node that ~/.proto/.prototools declares, and it sorts before install-herdr-integrations, which registers hooks for all three.
  • launchd LaunchAgents own gpg-agent and keyboxd, and no-autostart in ~/.gnupg/common.conf stops every client from starting one implicitly: a daemon started from a sandboxed agent shell keeps that sandbox for life and cannot reach the YubiKey. The GPG script writes and bootstraps those jobs in the before phase after Homebrew installation, because chezmoi needs the private keys while synchronizing encrypted files. It is run_onchange keyed on the Homebrew kegs those daemons link, so an upgrade of gnupg or of one of its libraries restarts them with the files now on disk. dirmngr is deliberately unmanaged, so keyserver operations fail explicitly instead of autostarting it; the script imports the card's public key over HTTPS from the URL stored on the card instead of using the card's fetch command.

A dedicated case-sensitive Code volume is provisioned as machine infrastructure rather than managed as a normal file target. It is unique, belongs to the APFS container that stores $HOME, and is mounted persistently by UUID through vifs. Provisioning must refuse to mount over a non-empty ~/Code or to proceed when ~/Code or the volume is mounted elsewhere, and must never unmount a volume automatically; a matching volume already mounted at ~/Code is reused whatever it holds. The run_once script provisions fresh-machine state rather than reconciling later drift; its Python test covers deterministic logic, while APFS, Disk Arbitration, and reboot acceptance remain manual Mac checks.

This repository uses Input Source Pro's exported JSON as the source of record instead of writing com.runjuu.Input-Source-Pro with defaults, because its app and website rules live in a Core Data store and many of its preferences are archived values. The import script first launches the app and waits for its process so the user can complete first-run Gatekeeper prompts, then dispatches inputsourcepro://import with the source file's path. URL imports replace settings without the GUI's confirmation and report success or failure in an app alert; open returning only proves URL dispatch. .chezmoitemplates is never deployed, so the export needs no ignore entry.

Package and tool ownership

Where a tool is declared follows from how much update lag its owner tolerates:

  • Coding agents tolerate none. claude and pi come from their vendor installers, unpinned, in one run_once script in the after phase; claude updates itself, while pi updates only when pi update runs, so the unattended-upgrades job invokes it. Pi keeps its managed install rather than its Homebrew formula or npm package because only the managed install pins its transitive dependencies. codex comes from the official homebrew/cask CLI cask in Brewfile, which moves its update onto the same job and reports brew upgrade --cask codex as its update action.
  • Agent-adjacent CLIs (ctx7, oracle, herdr) also sit in Brewfile; oracle comes from steipete/tap because it has no self-updater.
  • Background development tools (typescript, typescript-language-server, Pyright, mbx) are pinned in .github/dev-tools or the dev-tools installer and bumped by Renovate behind a release-age window.
  • Runtimes (node, bun, pnpm, go through proto; Rust through rustup) resolve through their own managers, with no pin and no window. The oracle and ctx7 formulae pull in Homebrew's node (and pnpm for oracle), which sits behind proto's shims on PATH and is not a runtime channel, so those CLIs execute a Node this repository does not pin.
  • GUI and daily-use apps are Brewfile casks, and App Store-only apps are mas entries. Logseq deliberately remains outside Homebrew ownership: this machine uses manually installed 0.10.15 because 2.x is incompatible, so its cask is not a fallback path.

The Brewfile installer runs brew bundle --no-upgrade, so an apply only installs missing entries: the unattended-upgrades job below upgrades formulae and casks, and the App Store updates mas apps, automatically only while its Automatic Updates setting is on, which this repository leaves to each machine.

Brewfile is the package declaration. run_onchange_after_setup-unattended-upgrades.sh writes the com.liby.unattended-upgrades LaunchAgent, which upgrades Homebrew packages, Pi, and the installed Rust toolchains, and loads it into the user's gui/$UID domain, on a 02:00/08:00/14:00/20:00 local-time calendar trigger plus RunAtLoad = true, which intentionally adds an immediate login or reload run. The script also writes the commands to an unattended-upgrades helper at ~/Library/Application Support/com.liby.unattended-upgrades/unattended-upgrades, and the plist runs that helper, so neither file is chezmoi-managed and the job is replaced only when the script's own content changes; a Brewfile change never reloads it.

The helper runs pi update after brew upgrade, and that step puts ~/.local/bin on PATH for the managed launcher and keeps the proto shims there for the Node it needs. It then runs rustup update, because rustup never refreshes an installed toolchain on its own, whichever channel it tracks. Both the Brewfile installer and this helper recursively clear com.apple.quarantine from the Codex cask after their package operation, including its code-mode and voice helpers: approval of the CLI does not approve those separate executables or carry over on upgrade. Homebrew can still prompt during installation when the cask runs codex completion before returning. The helper restarts gpg-agent and keyboxd when either daemon is missing or no longer reports the installed GnuPG version; a library-only upgrade leaves that version unchanged, so it is caught by the next chezmoi apply. The script writes that job itself rather than driving the domt4/autoupdate tap, which schedules only Homebrew commands and would drop the helper's Pi, Rust, quarantine, and GnuPG steps.

Every cask that declares auto_updates true, from Brewfile or installed by hand, is upgraded only by its own updater: dot_homebrew/brew.env sets HOMEBREW_NO_UPGRADE_AUTO_UPDATES_CASKS, which bin/brew loads into every brew process while XDG_CONFIG_HOME stays unset, so brew upgrade and brew bundle skip such casks without --greedy whether the job, the installer, or a shell runs them, and the cask definition rather than a repository list decides. Casks without that flag, such as anki, still upgrade through the job.

Brewfile owns the proto executable, while dot_proto/dot_prototools declares the tools and versions under PROTO_HOME that the dev-tools script installs; proto's standalone installer is not a fallback path. proto upgrade breaks that ownership in two places at once, so upgrade proto through Homebrew only. It writes proto and proto-shim into $PROTO_HOME/bin, which the tool shims and any shell that puts that directory first both resolve ahead of Homebrew, so later brew upgrade runs stop reaching the proto those callers execute. It also rewrites the directory of the running executable, because that executable sits outside proto's own inventory; under Homebrew that directory is the Cellar. Proto's own version check does not conflict with Homebrew ownership, because it only prints a notice pointing at proto upgrade and never installs anything; PROTO_VERSION_CHECK=0 silences it. An alias such as lts or latest is resolved again on every proto install, which bypasses the remote-version cache, and on every shim invocation, which re-enters proto run and reads a cache refreshed every 12 hours; the Node plugin maps both aliases to exact versions, so a moved target installs on the next proto install, and auto-install installs a resolved version that is missing. auto-clean runs after install, outdated, status, activate, and regen, and removes only tracked versions whose .last-used marker is older than 30 days, so a version some project still runs stays installed wherever the alias now points.

The exact global npm tool versions live in .github/dev-tools/package.json, Pyright lives in .github/dev-tools/requirements.txt, and mbx is pinned in the installer itself because no crate manifest exists for Renovate to read. Renovate groups these declarations with other dependencies, and one shared run_onchange installer installs the proto-declared tools and then reconciles each declaration independently without pruning unrelated machine-local npm, uv, or Cargo tools; do not split the installer. The dev-tools workflow supplies their status check; a file-scoped Renovate ignoreTests must not be restored because it can become branch-level configuration on a mixed dependency group. typescript, typescript-language-server, and Pyright back Claude Code's LSP plugins, which .chezmoitemplates/claude/settings.json enables; typescript-language-server falls back to the global typescript when a workspace's own cannot provide tsserver.

Each of these declarations carries a release-age cooldown, so a compromised upstream release has time to be reported and yanked before it is pinned here. npm and PyPI enforce it twice, and the two halves are coupled: the installer refuses to resolve an artifact younger than its window, so the matching Renovate rule in .github/renovate.json has to wait at least as long or it pins a version the installer cannot reach. dot_config/uv/uv.toml sets the PyPI window through uv's exclude-newer, and the installer renders the same value into its own uv tool install command rather than relying on uv discovering the deployed file. The installer sets the npm window through npm config set, which writes it to ~/.npmrc; an environment variable set in .zshenv would miss npm processes that do not inherit that environment. ~/.npmrc also carries registry tokens, so the source declares the setting through the command rather than tracking the file. Stable Cargo has no install-time filter, so the crate rule is mbx's only window. All windows are the same value, and one value is enough because the declarations hold only background tools that nobody waits on.

mbx is the machine's Cargo build cache. mbx setup installs a cargo shim that dot_zshenv puts ahead of ~/.cargo/bin, so every plain Cargo command, an agent's included, shares one store of compiled outputs. Each stored output is keyed on the complete rustc version and is never reused by another toolchain, and the unattended-upgrades job replaces a bare nightly every day, so keep rustup's default on stable, which moves on a six-week cycle; a project that needs nightly pins a dated one in its rust-toolchain.toml. A checkout's default target becomes a symlink to a managed directory that mbx removes when the checkout disappears, goes unused, or exceeds the disk budget, and mbx keeps the link out of git status through .git/info/exclude. Projects and agent instructions therefore need no mbx-specific steps. Two kinds of target directory stay unmanaged, and nothing prunes them: one chosen through CARGO_TARGET_DIR, --target-dir, or build.target-dir, so a project should keep Cargo's default location; and a real target that already exists on the Code volume, because mbx cannot move it across volumes and offers to delete it only in an interactive terminal, so delete it once. When a build result looks stale, rebuild with MBX_VERIFY=1 in a fresh target directory, which compiles beside the cache and compares the outputs; Cargo compiles nothing it considers up to date, so a plain rerun compares nothing.

.chezmoiexternal.toml pins the zsh plugin archives at fixed commits. The GitHub archive manager in .github/renovate.json updates those commits from each declared branch.

Repository validation

GitHub discovers workflow YAML only directly under .github/workflows, so keep that directory flat and give each file one independently triggered validation concern. Keep behavior tests beside their owner when chezmoi ignores the test filename; reserve .github/tests for repository-only tests that cannot safely live beside a managed source. Bun tests there use the *.test.ts name and run through bun test ./.github/tests/; Python tests use the conventional test_*.py name and must be reachable through unittest discover from the matching workflow; add a concern subdirectory only when multiple tests benefit from that grouping.

Every path-filtered workflow lists itself, the tests or validator it runs, and the production sources that can change the result. When a non-required workflow filters both pull_request and push, use a YAML anchor for the identical path lists. GitHub leaves the required check for a workflow skipped by pull-request path filters pending, so before making one required, trigger it for every pull request and move any filtering below the workflow level. Keep permissions read-only, disable checkout credential persistence, cancel superseded runs for the same ref, and give every job a finite timeout. Extract a reusable workflow only for a complete job shared by multiple callers, or a composite action for a meaningful repeated step sequence; do not hide isolated fields or one checkout step behind an abstraction.

The agent-instruction workflow runs for every pull request and every push to main because it checks local link targets plus the pre-action routing and credential consumer contracts that govern the whole repository, and can therefore become required without a path-filter skip deadlock.

Configuration ownership

Most source files and templates render to paths under $HOME. Standard source names and template mechanics follow chezmoi rather than repository-specific conventions.

Not every configuration is fully replaced. Applications such as Codex and Claude Code write mutable runtime state, so modify_ sources manage selected fields while preserving application-owned values.

Replace subtrees that source fully owns so that removed fields disappear from the destination. For shared subtrees, preserve application-owned fields through an overlay or explicit exceptions during replacement. Establish ownership from the application's writers rather than the shape of the configuration. An overlay preserves undeclared fields; removing a source declaration does not by itself retire its destination state.

Deleting a source file likewise leaves its existing destination in place, and chezmoi status no longer reports that unmanaged path. The repository avoids persistent deletion lists and migration markers so source describes the current configuration without accumulating past removals. The trade-off is that existing machines need explicit cleanup; .chezmoiremove would automate file deletion but not cleanup of fields in shared configurations.

The JSON and TOML modifiers compare parsed values before serializing, so reordering object keys in their fragments alone leaves the destination unchanged. Fragments embedded in scripts, such as codex/requirements.toml, also affect script trigger hashes; their ordering changes can rerun the consuming script.

Chezmoi source manages portable, durable preferences, including established interaction habits, when they should remain consistent across machines. It leaves matching convenience defaults implicit unless an explicit value enforces a repository boundary or counters observed write-back drift. Generated runtime state, machine-specific paths, and settings synchronized elsewhere remain owned by their existing systems.

The macOS defaults script owns the Apple UI preferences. Its whole-array writes replace a list wholesale, so a tile dragged into the Dock, a stack, or an input source added by hand does not survive a rerun, and its input source, language, and appearance values take effect at the next login rather than in the running session. Each key's own reason stays in that script's adjacent comment. The app setup script owns the third-party app preferences written with defaults, with each key's reason likewise in its adjacent comment, and registers login items through System Events, which writes the database the Login Items pane shows rather than a preference domain. A single fixed archived value, such as Xnip's shortcut, is written by that script as data; an app that stores many archived values and offers a readable export, such as Input Source Pro, is imported from that export instead.

Codex configuration

.chezmoitemplates/codex/config.toml declares the source-owned portion of ~/.codex/config.toml. The modifier replaces plugins, permissions.development, and each source-declared mcp_servers table, carrying forward only the destination's existing node_repl.env and node_repl.env_vars within those server tables. All other declared configuration is deep-overlaid. Replacing server tables removes stale transport fields that can make Codex reject the whole configuration. Removing an entire server from source still requires explicit destination cleanup. While default_permissions is set, every apply also deletes sandbox_mode and sandbox_workspace_write, because those legacy selectors can override the profile; that enforced absence is current configuration, not a record of a past removal.

Leave model, model_reasoning_effort, and per-app authorization out of source (its apps.* tables declare only each app's enabled flag) so changes made in Desktop, the TUI, or the live file survive an apply. Herdr integrations owns features.hooks. Desktop generates node_repl environment fields and can later replace its runtime tables, so an apply does not prevent subsequent application write-backs. Do not declare the legacy standalone mcp_servers.computer-use registration in source: source declarations can override the same-named plugin registration and conflict with Desktop-owned runtime state. Keep the bundled computer-use and unified-computer-use plugins enabled; unified-computer-use provides Desktop's cua_repl browser and computer runtime. Keep the bundled code-review plugin disabled: it adds a second GitHub path for pull-request comments and merges beside the gh and review skills.

Desktop persists registered preferences under [desktop] and removes their legacy global-state or persisted-atom copies after migration. Manage a registered preference in .chezmoitemplates/codex/config.toml; reserve the global-state fragment for current persisted atoms that have no registered Desktop setting.

~/.codex/.codex-global-state.json is also partially managed. dot_codex/modify_dot_codex-global-state.json overlays the portable global-state fragment, which keeps Mini's closed state, onboarding completion, dismissed announcements, active-line Git blame, Work home composer mode, and explicit review diff filter (branch) portable; every other global-state entry remains Desktop-owned. These keys belong to the current Desktop implementation rather than the public config.toml schema, so verify the installed app before changing or expanding their managed paths. A running Desktop process can overwrite an external apply with its in-memory persisted-atom values, so change these preferences through the UI or apply this target while Desktop is stopped.

Keep approval_policy = "on-request", approvals_reviewer = "auto_review", and the sole custom default_permissions = "development" profile together. .chezmoitemplates/codex/requirements.toml constrains that tuple because Desktop builds a new task's approval policy and reviewer from its saved agent mode rather than from config.toml (#33552): unconstrained, the saved auto mode yields on-request / user and full-access yields never / user. The constraint intentionally removes the built-in modes, including Full Access. Restart Desktop after changing that file: no reproduction has yet shown Desktop hot-reloading an edited requirements.toml. The Browser modifier deliberately overrides that posture for the Browser tool, granting no-prompt full-CDP control so an agent run is not gated on a human at the keyboard; the modifier owns the exact keys, values, and their invalidation condition.

The same requirements file registers guard-bash from ~/.codex/managed-hooks as the one PreToolUse Bash hook, and it hands each event to the shared agent guard.

The same requirements file enables a second managed hook, contextual-markdown-links, on UserPromptSubmit. It injects the Markdown link-placement instruction as additionalContext on every prompt because Codex ignores UserPromptSubmit matchers and the same context must cover links discovered after tool calls.

The Codex requirements script installs the rendered requirements as root-owned /etc/codex/requirements.toml because Codex ignores requirements in an unforced user preference such as a defaults write. The script needs sudo, so apply it from an interactive terminal.

The development filesystem profile remains standalone with :root = write, ~/.ssh denied except the exact config, allowed_signers, and known_hosts entries that ssh and Git signature verification use, exact read-only .env and .env.local entries under every workspace root, and no copied per-tool cache lists or broad command allows. Programs may consume credential and environment files through their normal interfaces; changing or deleting one still requires the user's explicit request. Filesystem capability does not authorize remote Git or secret-bearing chezmoi operations.

openai/codex#40254 tracks the current macOS reproduction in which Chrome fails trusted RPC path validation without an explicit Browser cache read rule. Keep ~/.codex/plugins/cache/openai-bundled/browser as the only extra read-only path so the trusted worker can load Desktop's runtime-owned Browser service; a chezmoi private_ or readonly_ source and chmod do not create a trusted permission root. Remove the exception only after deleting the source rule, applying the rendered configuration, fully restarting Desktop, confirming the effective permission table no longer contains the rule, and succeeding on the first Chrome initialization in a fresh task without a prior Browser attempt or retry.

Keep features.network_proxy = false with permissions.development.network.enabled = true so public and local networking remain direct while the filesystem profile stays enforced. Add proxy-only domains or allow_local_binding only when managed proxy enforcement exists; openai/codex#33227 documents the unresolved proxy-only local binding failure.

Both cli_auth_credentials_store and mcp_oauth_credentials_store stay on explicit keyring; auto may fall back to plaintext files. Before activating this on another machine, use a credential-safe status check against the exact store to confirm the existing login. Agents never migrate, rewrite, or delete the real store.

features.daemon_auto_start = false keeps Codex from starting a shared app-server daemon, so each TUI runs its own embedded app server. A shared daemon runs every attached session's hooks and tool commands with the environment of the terminal that first started it, so Herdr records sessions on the wrong pane or on none, and HERDR_* inside tool commands names a pane the agent is not in (herdrdev/herdr#4649, openai/codex#48500). A TUI still attaches to a daemon that is already running, such as one started before the setting was applied, and stopping that daemon leaves its TUIs offline rather than moving them to embedded servers. To retire it, wait until none of its sessions is working, quit each attached TUI and keep the codex resume <id> it prints, run codex app-server daemon stop, and resume each session in the pane it came from; the session's next turn runs the hook that reports that pane to Herdr again. codex queue does not need the daemon and still reaches a busy embedded session.

GitLab CLI configuration

glab reads its configuration from ~/.config/glab-cli, which dot_zshenv selects with GLAB_CONFIG_DIR instead of the macOS default under ~/Library/Application Support, because Claude Code's sandbox can write ~/.config but not ~/Library. glab writes its sign-in state into that config.yml, so the config modifier sets only the top-level preferences it declares, on every apply, and leaves the rest to glab. The glab setup script copies the private host's OAuth application ID from envchain with glab config set and runs glab's web sign-in only when glab has never signed in to that host.

Sign-in stores the tokens in that config.yml in plaintext instead of glab's default Keychain storage. glab must save new tokens at every refresh and refuses to refresh where it cannot, and Codex cannot run glab where the Keychain is writable: its sandbox denies the shared-memory write that a Keychain write makes, and while its profile denies ~/.ssh, escalation cannot move a command out of that sandbox. Keychain storage would therefore cost the ~/.ssh protection, yet it never kept the tokens from agents, which read them through glab inside their sandboxes; what plaintext gives up is the protection of copies of the file, such as backups. Each refresh invalidates the previous tokens, so a copy is useful only until the next refresh, unless a thief refreshes first and keeps the session; glab's next refresh then fails with invalid_grant. Sign in again with the setup script's login line: the bare glab auth login that glab auth status suggests puts the tokens back in the Keychain.

Claude Code configuration

.claude/rules/claude-code-settings.md is the path-scoped owner for Claude Code settings, sandbox, integration, and runtime-state decisions. Its path frontmatter makes Claude Code load it only for the matching sources; other Agents must follow the route in AGENTS.md before changing those sources.

Pi configuration

~/.pi/agent/models.json and ~/.pi/agent/keybindings.json are fully managed, while ~/.pi/agent/settings.json is partially managed. The settings modifier replaces each top-level key declared in the settings fragment, including the entire modelThinkingLevels map, and preserves undeclared keys. Applies intentionally overwrite UI-saved startup and per-model defaults so devices share the source configuration.

Temporary /model and /thinking selections remain available. After applying settings, use /reload, then select the model again with /model to activate the thinking defaults; resuming a session can restore its recorded model and thinking level. defaultProjectTrust = "always" intentionally lets every directory load project-local settings, resources, packages, and extensions without a trust prompt.

Authenticating the built-in anthropic provider exposes every Claude model in Pi's catalog, and models.json can only add or override models on a built-in provider, not hide the rest. enabledModels therefore keeps /model and model cycling to rc-gateway/** and the anthropic models in modelOverrides; update both lists together when adding or retiring a Claude model, and start a new session (/new or a new pi process) to apply a scope change because /reload does not re-resolve enabledModels. rc-gateway/** uses ** because Pi matches globs against provider/modelId with minimatch, where * stops at /, and the gateway's model IDs contain a vendor/model slash.

defaultTools = ["+codemode"] adds the built-in scripting tool to Pi's default tools without requiring an MCP server. Scripts can call classifier models through models.classify(); classifiers such as TypeSafe's Jev do not appear in the chat-model picker. The TypeSafe provider entry overrides only authentication, preserving Pi's built-in classifier catalog and System One protocol adapter.

thinkingLevelMap declares which thinking levels a model exposes, not preferred defaults. Derive it from published catalogs, without probing: the route's per-model reasoning_options from GET /api/v1/models supplies the levels, mapped to their own names (or the owner's level set in Pi's bundled provider catalog when that field is absent), and the model's OpenRouter catalog entry supplies off, matched by model family because route suffixes such as -fast are not part of the model id. Read the two independently: a level is what the route offers, off is whether the model can stop reasoning. Write null for a standard level (off/minimal/low/medium/high) neither source exposes, and omit an extended level (xhigh/max) neither exposes: an omitted standard level falls back to the provider's default mapping while an omitted extended level stays unsupported. Do not alias levels (minimal to low, or xhigh to max), and keep preferred defaults in settings.json. An HTTP 200 does not prove the requested effort was honored (gateway mappings, Fireworks API).

contextWindow in private_models.json.tmpl is this repository's working budget, not the window the route advertises. Pi compacts once the estimated context exceeds contextWindow - reserveTokens, checked before each request, and caps each request's output at the room left in the budget, so changing it moves the trigger and shrinks output space together. Default to 400000 so sessions compact before long-context quality degrades, within what the route accepts. Where the vendor bills the whole request at a higher rate past a prompt-size threshold (OpenAI 272K), size the budget so the trigger falls below it; the estimate is not a hard cap, so a request can still cross. Skip that when even the higher rate is cheap, because compacting early would discard context to save little. Set maxTokens to the smallest of contextWindow, the vendor's exact documented output limit (the integer its API reference gives, not a rounded "K"), and any cap the route actually enforces; this gateway's catalog max_tokens is not one, since DeepSeek returns more than its advertised 32768. Use contextWindow when the vendor documents no output limit. compaction.reserveTokens is 50000 rather than Pi's default 16384 because it also sets the summarization output budget (0.8 * reserveTokens): the default 13107 truncated once a session's merged summary and tracked-file list outgrew it. The reserve is global, so keep every contextWindow well above it or give the model a compaction.modelOverrides["provider/modelId"] entry; a reserve at or above the window makes the trigger non-positive and compacts after every tool batch. pi --list-models rc-gateway shows the loaded window and output limit but cannot validate thinkingLevelMap.

The rc-gateway provider sends a Codex CLI User-Agent because the gateway serves /api/v1/responses, which the openai-responses models use, only to that client and rejects Pi's own; the chat-completions models ignore the header.

strip-reasoning-filler.ts removes filler from the reasoning Pi replays to DeepSeek V4.1 Flash and recognizes the model only by its exact id, so renaming deepseek/deepseek-v4.1-flash in private_models.json.tmpl silently disables it; change both together.

Credentials, sessions, installed packages, and recorded trust decisions under ~/.pi/agent remain runtime-owned, so the directory must never be adopted wholesale. Both managed source directories stay private_ to preserve mode 0700. The gateway base URL comes from .private.rcGatewayBaseUrl; the rendered model file uses credential commands with the namespaces defined by the credential consumer contract, rather than carrying secrets.

Oracle configuration

Oracle reads ~/.oracle/config.json, and its oracle bridge client command rewrites that whole file as plain JSON with browser.remoteHost, browser.remoteToken, and browser.remoteViaSshReverseTunnel. The config modifier therefore manages only browser.modelStrategy and preserves the rest; a full-file replacement would delete Oracle-owned bridge state on the next apply. The oracle skill passes its browser flags explicitly, so this overlay covers only oracle invocations outside the skill. The deployed file can hold a plaintext token written by Oracle's bridge, so treat it as credential-bearing regardless of its visible contents, and inspect the overlay source instead of reading, diffing, or re-adding the destination.

Herdr integrations

run_onchange_after_install-herdr-integrations.sh.tmpl installs the Pi, Claude Code, and Codex integrations so each agent reports its native session ID to Herdr and a restarted Herdr server resumes the pane in its previous conversation instead of a bare shell. Herdr owns every file it writes, and herdr integration status lists them; the surprising one is features.hooks inside the otherwise managed ~/.codex/config.toml. Leave that key out of .chezmoitemplates/codex/config.toml. The installer owns the flag's name, having already migrated codex_hooks to hooks and deleted the retired key, so a source declaration would restore what a later migration removes. Omitting it costs nothing: the modifier compares parsed values, so the installed flag survives an apply and the flush-left line the installer writes never surfaces as drift. The rendered herdr --version in the script is the only refresh signal, because Herdr reports outdated integrations from herdr update, which a Homebrew upgrade bypasses. The Claude SessionStart registration is the one entry that must live in source because the settings modifier replaces the whole hooks key; .claude/rules/claude-code-settings.md owns its required form. The Codex hook reads its pane and Herdr socket from the environment of the process that runs it, which is why Codex configuration keeps Codex TUIs off a shared app-server daemon.

Agent guard

Brewfile installs the agent-guard package from Entwining/tap, providing /opt/homebrew/bin/agent-guard. The Claude Code settings register that executable for Bash and file tools. Codex requirements keep the hook under managed_dir; guard-bash checks the Homebrew binary and execs it with --runtime codex. The Pi extension maps Pi's tool calls to guard events and invokes the same executable.

The package owns guard rules, parser, and tests. This repository owns dot_ignore: its /Library/ entry keeps broad searches above that tree from enumerating it.

Shared agent instructions

Global behavior shared by Claude Code, Codex, and Pi lives in the ordered fragments under .chezmoitemplates/agents/. Each fragment owns its section heading, and every root includes each fragment once in the same order. Only the Claude root may append runtime-specific paragraphs; the Codex and Pi roots are assembled from the fragments alone, which the agent-instruction contract test enforces. Do not put a shared heading in a root or copy a shared paragraph there. Keep coding principles in their own fragment so evaluation can remove that rendered section without changing the remaining bytes. Rendered evaluation documents are derived evidence and do not belong in this repository.

Pi consumes the shared core through ~/.pi/agent/AGENTS.md, not APPEND_SYSTEM.md. The file's private_ source attribute keeps the rendered instruction file owner-readable only, matching the already-private agent directory. A project .pi/APPEND_SYSTEM.md replaces the global file of the same name, while global and project context files coexist in Pi's project context; using the context-file path preserves the global core when a project adds its own instructions.

Snowflake CLI execution

The installed Homebrew snow command has one native-SSO execution identity, reached through ~/.local/bin/snow, which keeps connector tokens out of the Keychain, so every call signs in again; that per-call sign-in is the chosen trade-off, not a fault to repair. Claude's permissions.allow and sandbox.excludedCommands own approval and exclusion, so on Claude a direct snow call runs outside the sandbox and opens the browser itself. On Codex no rule can move it out: a permission profile with any denied-read entry holds every command in the inner sandbox, which also defeats an exec-policy allow and an explicit escalation. snow therefore runs sandboxed there, and each sign-in emits the login URL for the agent's browser tool to open, which needs SNOWFLAKE_AUTH_FORCE_SERVER, loopback, and an identity-provider session in that browser. Dropping the ~/.ssh deny would not by itself restore the old outside-the-sandbox route: with the allow rule gone, an unescalated snow stays in the sandbox, so that route needs the allow rule back or an approved escalation on each call, and it costs the sandbox's protection of ~/.ssh. The personal snow skill owns query and same-task SSO behavior, and Snowflake RBAC owns server authorization. Do not narrow the route to snow sql -q or restore workspace uvx runtimes.

Managed skill registry

dot_agents/skills/<name>/SKILL.md is the canonical source layout for chezmoi-managed skills and is mirrored into ~/.agents/skills; Claude Code reads that mirror through the managed ~/.claude/skills symlink, so never add a skill under ~/.claude/skills directly. Resolve an installed path with chezmoi source-path and edit the source, not the deployed copy. Exclude unmanaged installed skills from bulk maintenance, change one only when the user names it, adopt one only when explicitly requested, and leave ~/.agents/.skill-lock.json as runtime-owned install state. Supporting references, scripts, and assets stay inside their owning skill; the registry has no index, manifest, or category directories. A skill adopted from a third-party source records its upstream tag and tree under metadata; adopt only the changes the CLI behavior actually requires, and treat that upstream text as a release-matched reference for current syntax, not permission to overwrite this repository's routing, authorization, or recovery policy.

In Claude Code's auto mode, a skill's allowed-tools skips classifier review during that skill's turn for the concrete non-interpreter CLI prefixes and tools its contract owns, while cross-turn or standing host grants live in ~/.claude/settings.json (see .claude/rules/claude-code-settings.md) and open-ended or interpreter commands stay on the sandbox and classifier path. If Claude denies a matching skill grant, preserve the contract and add the narrowest effective runtime exception backed by a reproducer.

Identity and encrypted data

.chezmoi.toml.tmpl defines prompts for account-specific template values. Each user supplies their own values during init; a fork should first review which fields still apply.

Encrypted files use GPG, with private keys stored on a YubiKey. Repository-only encrypted data can seed envchain namespaces in the macOS Keychain instead of deploying credentials as ordinary dotfiles. Adapting the full repository therefore requires replacing or removing its GPG recipients and personal encrypted data.

Agents may read the top-level ~/.ssh client config (config, config.*), public keys, allowed_signers, and known_hosts files, and nothing else under that directory. The agent guard and Claude's sandbox enforce that list by directory, so private keys deployed there need no naming convention and no per-key rule. The Codex sandbox reopens only the exact files ssh and Git signature verification need, because its read entries take exact paths or a trailing /** while git-style globs (*, ?, [, ]) are accepted only for deny; public keys stay unreadable inside that sandbox.

Git identity and signing

Both forges verify commits signed by the YubiKey, in different formats. GitHub verifies OpenPGP signatures from the card's signature key. GitLab verifies SSH signatures from the card's authentication key against that key as registered on the GitLab account, so the work address never becomes a user ID on the personal OpenPGP key: nothing about the employer is published with that key or has to be revoked from it later. Local Git verifies those signatures against the encrypted ~/.ssh/allowed_signers instead, which GitLab never reads. run_onchange_after_setup-gitconfig.sh.tmpl reads the key id from gpg --card-status and installs github.config with it and gitlab.config with the exported authentication key in Git's key:: form into ~/.config/git/, so signing reads no file under ~/.ssh; dot_config/git/config includes the GitHub file everywhere and the GitLab file for repositories under ~/Code/GitLab/. The script derives the key itself on every rerun because the GPG script reruns on a Homebrew keg change rather than a card change.

git-ssh-gpg-agent exists because gpg-agent serves the card's SSH key on ~/.gnupg/S.gpg-agent.ssh while shells keep the macOS ssh-agent in SSH_AUTH_SOCK. The wrapper points SSH_AUTH_SOCK at the gpg-agent socket, then runs ssh for core.sshCommand --transport or ssh-keygen as gpg.ssh.program, so Git transport and SSH signing reach the YubiKey from any shell, including sandboxed agent shells that inherit no agent environment. Bare ssh keeps the macOS agent and file keys, except on github.com and gitlab.* hosts, where IdentityAgent names the gpg-agent socket so the card's authentication key is reachable without Git. Changing that socket means updating that entry, the wrapper, and the socket allowances in Claude Code's sandbox and the Codex permission profile.

Credential-backed features

The seeding script reads .secrets/seed.asc and stores each entry in the macOS Keychain through envchain. Its plaintext is TOML with one top-level table per namespace and one environment variable per single-line string value; numeric and boolean-looking values remain quoted. The template below is the machine-checked contract for entries consumed by files in this repository; other seed entries are not repository requirements.

[claude-gateway]
ANTHROPIC_AUTH_TOKEN = "replace-with-value"
ANTHROPIC_VERTEX_BASE_URL = "replace-with-value"
ANTHROPIC_VERTEX_PROJECT_ID = "replace-with-value"
CLAUDE_CODE_SKIP_VERTEX_AUTH = "1"
CLAUDE_CODE_USE_VERTEX = "1"

[context7]
CONTEXT7_API_KEY = "replace-with-value"

[glab]
GITLAB_CLIENT_ID = "replace-with-value"

[pi]
ANTHROPIC_API_KEY = "replace-with-value"
RC_GATEWAY_API_KEY = "replace-with-value"

[typesafe]
TYPESAFE_API_KEY = "replace-with-value"

Each namespace's consumers:

  • claude-gateway: The Claude launcher injects the claude-gateway namespace only in gateway mode and otherwise unsets every variable the contract above declares for it, whatever set them, so a claude started inside a gateway session follows the persisted mode unless its own flag says otherwise. It is an executable on PATH ahead of the native install in ~/.local/bin, rather than a shell function, so the persisted mode also applies when Pi's bash, a wrapper such as timeout, or a script's subprocess runs claude.
  • context7: Every Context7 consumer goes through the Context7 skill, which runs the ctx7 CLI in envchain context7 so CONTEXT7_API_KEY reaches only the CLI child process. The namespace may also carry an optional CONTEXT7_BASE_URL, absent by default; point it, with no trailing slash, at a trusted reverse proxy that forwards the official /api/v2 query API, and add that host to Claude Code's sandbox allowedDomains. The global --base-url reaches query-mode requests, but upstream supports custom deployments only for MCP HTTP setup and treats on-premise query mode as unsupported. Validate a Context7 change by running a resolve-and-query through the skill.
  • glab: The glab setup script copies GITLAB_CLIENT_ID, the private GitLab's OAuth application ID under the variable name glab itself reads, from the glab namespace into glab's configuration.
  • pi and typesafe: The Pi model configuration reads ANTHROPIC_API_KEY and RC_GATEWAY_API_KEY from the pi namespace and TYPESAFE_API_KEY from the typesafe namespace. The Jev skill runs user-approved local TypeSafe experiments through envchain typesafe.

The agent instruction contract test parses the TOML block and verifies its exact keys, consumer paths, and local namespace wiring without reading the encrypted seed.