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’sdevenv.nixexports the same variable inenterShell, 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-processGH_CONFIG_DIRgives 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
- 1Password service accounts
- devenv.sh - source at cachix/devenv
- gitleaks
- The rest of the agent toolchain: Optimizing Claude Code