wihan/dev

writing / post

Using AI agents safely with 1Password and devenv

2026-09-01ai · agents · 1password · devenv · nix · security4 minmarkdown

Claude Code, and every agent like it, works by running shell commands. That’s the whole value - and the whole risk: whatever your shell can read, the agent can read. A token in .env, credentials in ~/.aws, a password in shell history - each is one cat away. And not just for the agent: the same is true for every npm postinstall script and every stray CLI you run.

The fix is not to trust promises. It is to make the blast radius small by mechanism: an agent session inside a project tree should hold only that project’s secrets and only that project’s identity, and nothing it does should be able to leak more.

Here’s the setup on my machine: 1Password scopes the secrets, devenv pins the toolchain, and gitleaks guards the exit.

1. One vault per project, one service account per vault

Project secrets live in a dedicated 1Password vault (wihan_dev for this stack) - never in the Personal vault. Create a service account (1Password → Developer → Service accounts) that can see only that vault, and save its token where only you can read it:

install -d -m 700 ~/.config/op
# paste the token 1Password shows you exactly once:
${EDITOR:-nano} ~/.config/op/wihan_dev.sa-token
chmod 600 ~/.config/op/wihan_dev.sa-token

A service account is headless - no desktop-app prompt, no biometrics - which is exactly what an agent needs. And it is scoped: if the token ever leaks, the damage stops at one vault of one project’s secrets.

2. Hand the token only to shells inside the project

This is the trick that makes it seamless. ~/.zshenv maps project directories to token files, so a shell that starts inside the tree exports the token - and a shell anywhere else gets nothing:

# ~/.zshenv - a directory-to-vault map for op service accounts
typeset -A op_service_accounts=(
  "$HOME/stuff/vanillauys" wihan_dev
)
for op_dir op_name in "${(kv)op_service_accounts[@]}"; do
  if [[ "$PWD" == "$op_dir" || "$PWD" == "$op_dir"/* ]] \
     && [[ -r "$HOME/.config/op/$op_name.sa-token" ]]; then
    export OP_SERVICE_ACCOUNT_TOKEN="$(<"$HOME/.config/op/$op_name.sa-token")"
    break
  fi
done
unset op_dir op_name op_service_accounts

Claude Code runs its shells in the project directory, so every session gets the right vault automatically - op read just works, no prompt. My personal vault stays behind the desktop app’s biometric unlock, which an agent cannot answer.

💡 devenv’s shell is bash and skips ~/.zshenv, so each project’s devenv.nix exports the same variable in enterShell, reading the same token file. One source of truth, two doors in.

3. Commit the references, never the values

Every repo gets a committed .env.tpl full of op:// references, and no .env at all:

# env/.env.tpl - committed; documents every key, contains no secret
DOKPLOY_API_KEY=op://wihan_dev/dokploy_api_key/credential
CLOUDFLARE_API_TOKEN=op://wihan_dev/cloudflare_wihan_dev/api_token
op run --env-file=env/.env.tpl -- terraform plan

op run resolves the references at launch, injects the values into that one process, and scrubs them from any output it captures. Nothing lands on disk, nothing sits in history, and the agent can run the deploy without a secret ever appearing in a file it could re-read.

4. Pin the toolchain - and the identity - with devenv

devenv (by the Cachix folks) declares a per-directory Nix environment. The obvious win is deterministic tools: the agent gets the same bun, terraform, and go as CI. The subtle win is per-process environment:

{ pkgs, ... }:
{
  # Pin gh to the personal account for this whole tree. Per-process,
  # so other terminals keep their own account.
  env.GH_CONFIG_DIR = "/home/wihan/.config/gh-vanillauys";

  packages = [
    pkgs.bun
    pkgs.terraform
    pkgs.gitleaks
    pkgs.go
  ];
}

I have two GitHub accounts, work and personal. GH_CONFIG_DIR points gh at a profile directory whose hosts.yml names the right account, so an agent in this tree cannot accidentally act as the work account.

⚠️ Never let an agent run gh auth switch. It mutates global state and changes the active account in every other terminal - including the one where a work session is running. A per-process GH_CONFIG_DIR gives each tree its identity without touching the others.

5. Scan every commit on the way out

Scoping limits what an agent can read. gitleaks limits what it can publish: a global pre-commit hook scans the staged diff of every commit - agent-made or not - before the commit exists:

# ~/.config/git/hooks/pre-commit
gitleaks git --pre-commit --staged --redact --no-banner || {
  echo "gitleaks found a possible secret in the staged changes." >&2
  exit 1
}
git config --global core.hooksPath ~/.config/git/hooks

The line you want to see under every agent commit: no leaks found.

Verify the boundaries

From a shell inside the project tree:

op whoami                  # ✅ the service account, not your user
op vault list              # ✅ exactly one vault
gh api user --jq .login    # ✅ the pinned account

And from a shell outside it:

op vault list              # ❌ falls back to the desktop app - no ambient token

If all four line up, an agent session can deploy the whole stack and still cannot touch your personal vault, your work identity, or a secret on disk - because none of those exist where it lives.

Further reading