# Using AI agents safely with 1Password and devenv

> An agent that runs shell commands can read whatever your shell can. Scope the secrets to one vault, pin the toolchain per directory, and let it work.

- date: 2026-09-01
- tags: ai, agents, 1password, devenv, nix, security

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](https://developer.1password.com/docs/service-accounts/) scopes the
secrets, [devenv](https://devenv.sh) 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](https://developer.1password.com/docs/service-accounts/)
(1Password → Developer → Service accounts) that can see only that vault, and
save its token where only you can read it:

```bash
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:

```zsh
# ~/.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:

```bash
# 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
```

```bash
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](https://devenv.sh) (by the [Cachix folks](https://github.com/cachix/devenv))
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:

```nix
{ 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:

```sh
# ~/.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
}
```

```bash
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:

```bash
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:

```bash
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

- [1Password service accounts](https://developer.1password.com/docs/service-accounts/)
- [devenv.sh](https://devenv.sh) - source at [cachix/devenv](https://github.com/cachix/devenv)
- [gitleaks](https://github.com/gitleaks/gitleaks)
- The rest of the agent toolchain: [Optimizing Claude Code](/tools)