Devbox automation keeps always-on agent users reproducible without making their secrets or identities part of the public dotfiles repo.
Tracked here:
- portable tools in
BrewfileandBrewfile.devbox - shared shell, Git, SSH, mise, editor, and Codex defaults
- public-safe scripts and validation
- templates and contracts for local service setup
Local only:
- Git identities, signing keys, and 1Password SSH vaults
- Infisical workspace/project auth and service tokens
- workspace env values and local service state
- process-compose ports when they are identity-specific
- Codex auth, trusted paths, sessions, and agent-rule symlinks
Environment variables are not a secret boundary. Anything running in that process tree can read them.
Humans use both 1Password and Infisical. Agents use Infisical only for secrets/env access. 1Password remains the human/manual vault for account credentials, recovery material, human SSH key material, and other secrets that should not be ambiently available to agents. Devbox agent SSH key material may live in Infisical under the devbox boundary when the agent needs to retrieve or write it.
When an agent task needs shared env, check the relevant Infisical project/path
first. Do not recreate workspace .env symlinks, devbox-env generated files,
token caches, or 1Password service-account refresh stacks.
For services and agents:
- Create or choose the correct Infisical project and environment for the runtime.
- Create a machine identity in the same Infisical organization and grant it access to that project/path.
- Configure the devbox user once with
./scripts/secrets/configure-infisical-devbox.sh. - Use Universal Auth to mint short-lived machine tokens at command time. Do not keep a human Infisical CLI session on agent devboxes.
- Keep raw client secrets and access tokens out of shell startup, launchd plists, process-compose YAML, tracked files, and long-lived interactive shells.
- Run the env-consuming runtime through
./scripts/secrets/infisical-devbox-run.sh -- <command>. The runtime owns how it reads Infisical and sets itself up. - Keep any identity-specific Infisical project, environment, path, client ID, or client secret values out of this repo.
Infisical access tokens are short-lived. A devbox that must keep working across future agent shells and reboots needs the Universal Auth client credentials in owner-only local machine state. This is an intentional tradeoff: the credentials are persistent on that Unix account, but they are not shell exports, process-compose config, launchd config, tracked files, or generated runtime dotenv refresh stacks.
One-time setup:
./scripts/secrets/configure-infisical-devbox.shThe helper prompts locally for the Infisical domain, project ID, environment, Universal Auth client ID, and Universal Auth client secret. Use the client ID shown under the machine identity's Universal Auth method, not the machine identity ID shown in the identity details panel. Humans should source those values from their secret manager, usually 1Password or Infisical, and enter them locally. Do not paste them into agent chat.
It writes non-secret selectors to ~/.config/uinaf/devbox.env and machine
credentials to ~/.config/uinaf/infisical-machine.env. Both files must be mode
0600. The helper refuses to continue when the Infisical CLI has an
authenticated human user session and verifies the machine identity can mint a
token before writing config. It does not persist secret paths; paths belong at
the command boundary.
Routine command-boundary use gives one child command a short-lived machine token and the configured Infisical project selectors:
./scripts/secrets/infisical-devbox-run.sh -- <repo-owned-secret-command>The child command receives INFISICAL_TOKEN, INFISICAL_DOMAIN,
INFISICAL_PROJECT_ID, and INFISICAL_ENV. If the caller sets
INFISICAL_SECRET_PATH, the runner forwards it too. Dotfiles does not render
app env, generate runtime dotenv files, or know another repo's secret path.
Repo-local setup example:
INFISICAL_SECRET_PATH=/example-repo/runtime \
~/projects/uinaf/dotfiles/scripts/secrets/infisical-devbox-run.sh -- \
make secrets-setupThe target repo owns make secrets-setup: it may run infisical export, use
infisical run, write an ignored 0600 local file, or avoid disk entirely.
OpenClaw-shaped example:
INFISICAL_SECRET_PATH=/example-devbox/openclaw-env \
~/projects/uinaf/dotfiles/scripts/secrets/infisical-devbox-run.sh -- \
openclaw <env-render-or-run-command>The real OpenClaw path and command belong in the workspace or OpenClaw docs, not in this public repo.
Small AGENTS.md snippet for a repo that frequently needs runtime secrets:
## Secrets
On agent devboxes, use Infisical through the dotfiles runner:
`INFISICAL_SECRET_PATH=/this-repo/runtime ~/projects/uinaf/dotfiles/scripts/secrets/infisical-devbox-run.sh -- make secrets-setup`
Do not use 1Password, workspace `.env` symlinks, or committed/generated secret
files for agent runtime env.Do not add dotfiles scripts that render another repo's secrets. If a consumer repo needs a better secret setup flow, add the wrapper to that repo.
If setup fails with Invalid credentials, check that the ID came from the
Universal Auth method. The machine identity ID in the details panel is not a
Universal Auth client ID and cannot mint a token.
Devbox agent SSH keys may be stored as Infisical secrets. This keeps the key in the same human-plus-agent sharing system as runtime env while avoiding 1Password service-account plumbing in agents.
Use this shape:
- Store the private key under the devbox or agent secret boundary that needs it.
- Store the public key, fingerprint, and key type beside it.
- Store a base64 copy when a shell or CLI path needs a single-line value.
- Grant the machine identity only the project/path it needs.
- Retrieve the key only into the command environment or an owner-only local
key file, then set mode
0600.
The retrieval command shape is:
SSH_PRIVATE_KEY_B64_SECRET=EXAMPLE_SSH_PRIVATE_KEY_B64
INFISICAL_SSH_SECRET_PATH=/example-devbox/ssh
SSH_IDENTITY_FILE="$HOME/.ssh/example"
INFISICAL_SECRET_PATH="$INFISICAL_SSH_SECRET_PATH" \
SSH_PRIVATE_KEY_B64_SECRET="$SSH_PRIVATE_KEY_B64_SECRET" \
./scripts/secrets/infisical-devbox-run.sh -- \
sh -c '
infisical secrets get "$SSH_PRIVATE_KEY_B64_SECRET" \
--domain "$INFISICAL_DOMAIN" \
--token "$INFISICAL_TOKEN" \
--projectId "$INFISICAL_PROJECT_ID" \
--env "$INFISICAL_ENV" \
--path "$INFISICAL_SECRET_PATH" \
--plain \
--silent
' | base64 --decode > "$SSH_IDENTITY_FILE"
chmod 600 "$SSH_IDENTITY_FILE"
ssh-keygen -y -f "$SSH_IDENTITY_FILE" | ssh-keygen -lf -Verify by fingerprint only. Do not print private keys, paste key values into chat, commit key material, or keep Infisical client secrets in shell startup, launchd, process-compose, tracked files, or long-lived shells.
Before treating a devbox as agent-ready:
- Verify the machine identity can list or export the intended project/path.
- Verify
infisical login status --domain https://eu.infisical.com/apihas no authenticatedusersession. - Verify no default shell exports Infisical tokens or machine credentials.
./scripts/verify/devbox-services.sh checks the Infisical CLI, owner-only
config modes, and machine identity token minting. Set
INFISICAL_SECRET_PATH=/some/path only when you want that check to also prove
access to a specific command-boundary path. A missing persistent machine
identity config fails by default. Set INFISICAL_MACHINE_AUTH_REQUIRED=0 only
for repo-local smoke checks on a machine that is not acting as an agent devbox.
Use secret-manager projects and vaults to model capability boundaries. A boundary should answer "which runtime needs this secret?" rather than "which human knows about this project?"
Use this generic split:
| Context | Vault | Access |
|---|---|---|
| Human operations | 1Password and Infisical | Humans only. |
| Shared env | Infisical <context> project |
Humans and approved agent identities. |
| Devbox agents | Infisical identity scoped to the devbox user | Env and SSH key material for that devbox identity only. |
| CI | <context>-ci |
GitHub Actions or the relevant CI runtime only. |
| Shared CI lane | <lane>-ci |
Only the CI jobs for that lane. |
Do not share service tokens across these boundaries. CI, devbox agents, and humans are different runtimes and should get different credentials even when they work on related projects. Store bootstrap/recovery credentials where humans can rotate them, not inside the same runtime scope the credential reads.
Shared devboxes may host multiple agent identities. Keep their Unix users, Git identity, GitHub auth, SSH keys, Codex/Claude config, trusted project paths, workspaces, and Infisical access separate.
The goal is to avoid ambient cross-context access. A compromised package, agent session, or service in one identity should not automatically get another identity's Slack, GitHub, CI, or Infisical capabilities.
If an identity needs access outside its normal context for a specific task, grant it explicitly and temporarily, then remove that access after the task.
For GitHub, devbox repos should use SSH remotes. A human should provision the
per-user GitHub key into an owner-only local key file during bootstrap.
configure-git.sh --profile devbox writes a Host github.com override in
~/.ssh/config.local when the signing key is a local path. That override uses
the local key file directly and sets IdentityAgent none for GitHub only.
Each devbox user should have a local config file outside Git:
DEVBOX_USER=example
PROCESS_COMPOSE_SOCKET="/Users/example/.local/run/process-compose.sock"
INFISICAL_DOMAIN=https://eu.infisical.com/api
INFISICAL_PROJECT_ID=example-project-id
INFISICAL_ENV=devThe file should be mode 0600. Persistent machine credentials live separately
in ~/.config/uinaf/infisical-machine.env, also mode 0600:
INFISICAL_CLIENT_ID=...
INFISICAL_CLIENT_SECRET=...Keep both files out of Git. Do not create repo-local workspace .env symlinks
for agent runtime env.
If a devbox identity does not run process-compose, set
PROCESS_COMPOSE_ENABLED=0 in its local config so verification does not
accidentally query another user's supervisor.
Use process-compose as the per-user supervisor for always-on agent services when the identity needs it. Launchd should start process-compose; process-compose should own service restart policy, logs, health checks, and one-shot tasks.
Prefer a per-user Unix socket over a shared localhost TCP port:
~/.local/run/process-compose.sock
Do not reuse another identity's process-compose port or socket.
Do not put secrets directly into launchd plists or process-compose YAML. Use Infisical at the command boundary for workspace/app env.
Run the normal bootstrap check for each user:
./scripts/verify/bootstrap.sh --profile devboxDevbox Git config includes /opt/homebrew as a safe directory so local admin
users can operate on the shared Homebrew prefix without Git dubious-ownership
failures.
Run the devbox-specific boundary check for each devbox user:
./scripts/verify/devbox-services.shThat check verifies process-compose, default shell auth exports, Infisical CLI
availability, persistent machine credential file permissions, and configured
machine identity access. It also fails if the Infisical CLI has an
authenticated human user session on the devbox.
Run the devbox security audit for each devbox user:
./scripts/audit/devbox.shThat audit is stricter than verification. It checks stale secret-looking backups, Git/GitHub identity, SSH key permissions, GitHub SSH auth, admin group drift, Tailscale health, and local service config.
Treat prose audit output as sensitive. Maintained scanners can include matched
secret material when they report a verified leak, so use --json for remote
collection and summarize findings by detector type, file path, and line number.
If the audit reports that direct MagicDNS works but the system resolver is not using Tailscale DNS, restart the Homebrew Tailscale daemon from a local admin session:
sudo launchctl kickstart -k system/homebrew.mxcl.tailscaleThen rerun ./scripts/audit/devbox.sh. The repaired resolver should resolve
Tailscale short hostnames through normal system lookup, not only through direct
queries to 100.100.100.100.
If the daemon restart does not restore resolver wiring, recreate the resolver files explicitly:
sudo mkdir -p /etc/resolver
printf 'nameserver 100.100.100.100\nsearch <tailnet>.ts.net\n' \
| sudo tee /etc/resolver/search.tailscale >/dev/null
printf 'nameserver 100.100.100.100\n' \
| sudo tee /etc/resolver/<tailnet>.ts.net >/dev/null
printf 'nameserver 100.100.100.100\n' \
| sudo tee /etc/resolver/ts.net >/dev/null
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponderFor broad OS-level posture, run ./scripts/audit/host.sh. It uses
Lynis as a maintained host scanner and keeps full reports out of the repo by
default.
For compliance-style OS posture, run ./scripts/audit/repo.sh after
generating a macOS Security Compliance Project check-only script for the host's
macOS version. Start with check-only results and review exceptions before
applying any remediation outside this repo. See
Security audits for the full audit model.