Reference architecture for a safe small teamWeekend by weekend. No security hire required.
Enterprise security guides assume a team of people who do nothing else. This one assumes two engineers, a deadline, and a Claude Code session already open. Here's what to actually do.
-
01
Today
Ambient creds
Agents inherit your full shell environment
.env · ~/.zshrc · keychain -
02
30 days
Vault + ignore lists
Secrets leave the shell, files leave the repo
1 vault · <10 min setup -
03
90 days
Broker + audit trail
Agents request access, grants are logged
zero ambient surface
TL;DR· the answer, in twenty seconds
What: Small teams running coding agents typically start with secrets in environment variables and .env files. Agents inherit everything in the shell and write state files back into the repo. That surface has already produced real leaks.
Fix: Three weekend investments close the major gaps: (1) ignore lists and a local vault, (2) a broker process so agents request secrets instead of inheriting them, (3) CI guardrails that catch regressions. None of this requires a security hire.
Lesson: The security posture that matters for a small team is the one that prevents the most likely incidents, not the most spectacular ones. Leaked credentials beat prompt injection every time in both frequency and cost.
The security guides aimed at software teams were written for companies with a CISO, a security architect, and a dedicated person who reads threat-model documents for fun. If that's not you, those guides don't help. They leave you with a list of enterprise controls you can't implement and no idea what to do this weekend.
This guide is for a team of two to ten. You're running coding agents, probably Claude Code or Codex CLI, probably on developer laptops with .env files in most of your project roots. You don't have a security hire. You might not have a shared secrets manager at all yet. You want to be meaningfully safer without adding a full-time job to your week.
The architecture below is a 90-day build. Three weekends of a few hours each gets you from "ambient everything" to a posture that would have prevented the major coding-agent incidents of the past six months. You don't need all three. Stop wherever the risk/effort ratio flips.
What to know in 60 seconds
- Coding agents inherit your full shell environment and can write state files back into your repo. Knostic found Claude Code's
settings.local.jsonin about 1-in-13 npm packages in February 2026. - GitGuardian's 2026 State of Secrets Sprawl report put AI-assisted commits leaking at twice the baseline rate.
- The fixes with the most impact are about where secrets live before the agent session starts, not about tooling choices.
- A local encrypted vault replaces
.envfiles. A broker injects specific credentials into child processes at call time instead of leaving them ambient in the shell. An append-only audit log answers "what did the agent touch and when." - SOC 2, SIEM, and zero-trust identity proxies are out of scope for this guide. None of them are weekend projects, and none of them address the actual threat model for a 5-person team.
The starting point
Most small teams running coding agents start from the same place.
Secrets are in ~/.zshrc or ~/.bashrc as export STRIPE_KEY=... lines, or in per-project .env files that get loaded at the start of a dev session. The .env file is in .gitignore, but the .env.example file has real values half the time. A few keys live in the macOS Keychain. Some live in Vercel or Railway's dashboard and also in a Notion doc someone made during onboarding.
When a developer runs a coding agent, that agent inherits the full shell environment. It can read STRIPE_KEY, OPENAI_API_KEY, DATABASE_URL, and anything else currently exported. The agent writes state files into the project directory, including .claude/settings.local.json, .cursor/, and equivalents for every other tool. Those files go into git unless the ignore lists are specifically configured to stop them.
The architecture in diagram form:
+------------------+
| developer box |
| |
| ~/.zshrc | exports STRIPE_KEY, DATABASE_URL, etc.
| .env (project) | loaded per-session
| |
| coding agent | inherits full env at launch
| └─ writes ──► .claude/settings.local.json (into repo root)
| |
+------------------+
|
| git push (if .claude/ not in .gitignore)
▼
+------------------+
| git remote | state file in commit history
+------------------+
|
| npm publish / docker build / pip publish
▼
+------------------+
| public artifact | .claude/ in tarball unless .npmignore says otherwise
+------------------+
Nothing here is unusual. It's how most credential leaks start too.
Where to spend your first weekend
Work through the leak surface that already exists before adding anything new.
Get ignore lists right, everywhere
Every project that uses a coding agent needs these paths blocked from git and from any publishing pipeline.
# In every project root
echo ".claude/" >> .gitignore
echo ".cursor/" >> .gitignore
echo ".aider/" >> .gitignore
echo "*.local.json" >> .gitignore
# For npm packages
echo ".claude/" >> .npmignore
echo ".cursor/" >> .npmignore
echo ".aider/" >> .gitignore
# For Python packages (add to MANIFEST.in)
echo "recursive-exclude .claude *" >> MANIFEST.in
echo "recursive-exclude .cursor *" >> MANIFEST.in
# For Docker
echo ".claude/" >> .dockerignore
echo ".cursor/" >> .dockerignore
echo ".env" >> .dockerignore
Check whether you've already committed any of these:
git log --all --oneline -- '.claude/*' '.cursor/*' '*.local.json'
Any output means the file is in your history and potentially in your published artifacts. Pull the last five npm tarballs and grep them:
npm pack mypackage@1.2.3
tar -tf mypackage-1.2.3.tgz | grep -E "\.claude|\.cursor|\.env"
If you find anything, rotate the credentials. Don't delete the file. Delete does nothing to history or to already-published tarballs. Rotate the credential at the provider, not in the file.
Replace .env files with a local vault
.env files get committed, they get copied to the wrong machine, and they persist long after the credential has been rotated at the provider. A local encrypted vault stops all of that.
Pick one vault for the whole team. pass is GPG-backed and free. 1password-cli needs a team account. Either works. The workflow is the same regardless:
# Put the secret in the vault once
pass insert stripe/live-key
# Load it into a process at call time, not at shell startup
STRIPE_KEY=$(pass stripe/live-key) node server.js
# Or for a coding agent session
STRIPE_KEY=$(pass stripe/live-key) claude code
The agent gets the value for that process only. When the process exits, the value is gone. Nothing writes it to .zshrc. Nothing leaves it lying in a file the agent might read.
Remove the export STRIPE_KEY=... lines from ~/.zshrc and ~/.bashrc. Yes, this means some things break immediately. That's the point. Anything that breaks was using ambient credentials it should have been requesting explicitly.
The 30-day architecture
+------------------+
| developer box |
| |
| encrypted vault | single source of truth
| (pass / 1pw) |
| |
| coding agent | receives specific keys at launch only
| └─ writes ──► .claude/ (in .gitignore, never in git)
| |
+------------------+
|
| git push (.claude/ blocked)
▼
+------------------+
| git remote | no state files in history
+------------------+
Where to spend your second weekend
The broker pattern replaces direct vault loading. A broker process sits between the vault and the agent. The agent requests a credential by name; the broker decides whether to grant it and logs the decision.
Why bother with a broker
Direct vault loading (STRIPE_KEY=$(pass ...) claude code) puts the value in the agent's environment for the whole session. The agent can read it any time. If it writes debug output, the value ends up in a log. A 4-hour session means 4 hours of ambient exposure.
A broker injects the value into a specific child process, only when that process is launched. The agent doesn't see the value between calls. The log shows what was granted and when.
The broker pattern
+------------------+
| developer box |
| |
| encrypted vault |
| | |
| ▼ |
| broker process | receives requests, decides, logs grants
| | |
| ▼ |
| child process | receives injected env var, exits
| (one per call) | value gone when process exits
| |
| audit log | append-only, one line per grant
+------------------+
A minimal broker is a shell wrapper. No daemon, no network service:
#!/usr/bin/env bash
# ~/bin/with-secret
# Usage: with-secret VARNAME=vault/path -- command args
set -euo pipefail
PAIR="$1"; shift # e.g. "STRIPE_KEY=stripe/live-key"
VAR="${PAIR%%=*}"
PATH_IN_VAULT="${PAIR##*=}"
COMMAND=("${@#--}")
VALUE="$(pass "$PATH_IN_VAULT")"
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) GRANT $VAR -> ${COMMAND[0]}" >> ~/.local/share/agent-audit.log
env "$VAR=$VALUE" "${COMMAND[@]}"
# Grant stripe key to a single curl call
with-secret STRIPE_KEY=stripe/live-key -- curl -s https://api.stripe.com/v1/charges \
-H "Authorization: Bearer $STRIPE_KEY"
# Start a Claude Code session with a specific key injected
with-secret OPENAI_API_KEY=openai/dev-key -- claude code
The audit log at ~/.local/share/agent-audit.log is append-only plain text. Not HMAC-chained, not tamper-evident. It answers the question you'll actually ask after something goes wrong: "what did we grant and when."
MCP server allow-lists
OX Security reported roughly 7,000 MCP servers in the wild as of early 2026, with about 150 million total downloads and no code-signing requirement. An MCP server runs with your credentials. Treat it like a dependency.
Keep an explicit allow-list of MCP servers your team uses. Any server not on the list gets explicitly blocked in .claude/settings.json (the shared project config, not settings.local.json):
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"]
}
},
"experimentalMcp": {
"allowUnlisted": false
}
}
Review this file in code review the same way you'd review a new package.json dependency. An MCP server that silently reads STRIPE_KEY from the environment and exfiltrates it is a real attack vector, not a theoretical one. The MCP GitHub prompt-injection data heist documented by Snyk researchers in early 2026 showed credentials leaving a developer's machine through a malicious server injected via a prompt.
Where to spend your fourth weekend
CI guardrails and a rotation schedule are the last gaps to close.
CI guardrails
CI runners use ephemeral filesystems. The state file the agent writes during the job dies with the job. That's good. What CI doesn't automatically do is catch state files that sneak into the artifact.
Add a pre-publish check to every pipeline that ships a public artifact:
# .github/workflows/publish.yml (relevant step)
- name: Block agent state files from artifact
run: |
for f in .claude settings.local.json .cursor .aider; do
if [ -e "$f" ]; then
echo "ERROR: $f found in workspace. Refusing publish."
exit 1
fi
done
For npm, add prepublishOnly to package.json:
{
"scripts": {
"prepublishOnly": "bash -c 'test ! -e .claude && test ! -e settings.local.json || (echo BLOCKED && exit 1)'"
}
}
Pre-commit hooks
Pre-commit hooks run before git commits, not before publishes. They are faster to hit and easier to bypass (git commit --no-verify). Use them as an early warning, not a final gate:
# .git/hooks/pre-commit
#!/usr/bin/env bash
if git diff --cached --name-only | grep -qE '\.claude/|settings\.local\.json'; then
echo "BLOCKED: agent state file staged for commit"
exit 1
fi
chmod +x .git/hooks/pre-commit
Or use pre-commit (the Python tool) with a local hook so the check travels with the repo:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: block-agent-state
name: Block agent state files
entry: bash -c 'git diff --cached --name-only | grep -qE "\.claude/|\.cursor/|settings\.local\.json" && echo BLOCKED && exit 1 || true'
language: system
pass_filenames: false
Rotation cadence
Pick a cadence and put it in your calendar. The right cadence depends on what the key does. A production Stripe key and a dev-only OpenAI key have different risk profiles.
A workable starting point:
- Production credentials: rotate every 90 days, or immediately after any agent session you'd describe as "unusual"
- Third-party API keys with billing exposure: rotate every 90 days
- Development and staging keys: rotate every 180 days or when an engineer leaves
- After any public incident involving the vendor: rotate within 24 hours regardless of cadence
Put ROTATE_CREDS in your shared calendar on the first Monday of each quarter. Fifteen minutes with the vault open. Not glamorous. Works.
What you don't need yet
Knowing what to skip saves as much time as knowing what to do.
SOC 2. SOC 2 is a compliance audit framework. It costs $20,000-$80,000 in auditor fees plus 6-12 months of prep work. It does not make your credentials more secure. It makes your security posture legible to enterprise buyers. If you don't have enterprise buyers asking for it, you don't need it.
SIEM. A security information and event management system is designed to aggregate logs from dozens of services and find anomalies at scale. At 5 people, the anomaly-detection value is near zero. You have 3 services and a Slack channel. You'll notice the anomaly in Slack.
Zero-trust identity proxy. Products like Teleport, Boundary, or a self-hosted Pomerium are genuinely good. They're also configured and operated by a person who does that. If you have a DevOps engineer who wants to run one: great. If you don't: skip it. The vault-plus-broker pattern from weekend two gets you 80% of the access-control benefit without the operational burden.
Hardware security keys for every service. Mandatory FIDO2 on everything is the right long-term posture. It's also a 2-day project per service because half your SaaS tools have inconsistent MFA policies. Do it incrementally: start with GitHub, Vercel, your cloud provider, and your secrets vault. That's the 80%.
Formal threat model. Writing a threat model document is useful when it changes your decisions. At the start, the threats are obvious: leaked credentials, supply-chain compromise, agent state files in git. You already know those. Writing them in a document first delays the fixes.
When to skip the security upgrade and ship instead
The 90-day plan above is real work. Each weekend is 4-8 hours: setup, testing, updating workflows. For a 3-person team mid-launch, that's real time.
A workable triage:
If you have no public packages and your git repos are private, the first-weekend work (ignore lists) takes 30 minutes per repo. Do it now. The cost is trivial and the incident risk is real.
If you have public packages, do the ignore lists and the prepublishOnly check before your next release. The Knostic disclosure in February 2026 found 1-in-13 npm packages leaking agent state. That's not a tail risk. That's a high base rate.
If you're pre-launch with a private repo and no production credentials in your development environment yet, you can defer the vault and broker work until you're handling real user data. Spending a weekend on the broker pattern before you have production keys is optimization ahead of the problem.
The threshold is: do you have production credentials on developer laptops? If yes, do the vault work. If no, do the ignore lists and schedule the vault work for the week before you go to production.
Pick the highest-value fix for the moment you're actually in.
A 90-day checklist
## Small-team agent security (90-day plan)
### Weekend 1: Ignore lists and vault (do this first)
- [ ] .claude/, .cursor/, .aider/, *.local.json in .gitignore for every active repo
- [ ] Same paths in .npmignore / MANIFEST.in / .dockerignore for every published artifact
- [ ] git log --all -- '.claude/*' clean across all repos
- [ ] Last 5 published tarballs checked for agent state files
- [ ] .env files removed from git history wherever found
- [ ] Credentials in found .env files rotated
- [ ] Single vault chosen and installed (pass, 1password-cli, or equivalent)
- [ ] export lines removed from ~/.zshrc / ~/.bashrc
- [ ] Each project's secrets loaded from vault at call time, not at shell init
### Weekend 2: Broker and MCP allow-list
- [ ] Shell wrapper or tool in place to inject secrets into child processes
- [ ] Audit log capturing grant events (even plain text to start)
- [ ] MCP server allow-list in .claude/settings.json for every active project
- [ ] MCP allow-list reviewed by at least one other team member
- [ ] Agent state files confirmed not reaching any publish pipeline
### Weekend 4: CI and rotation
- [ ] CI step that exits non-zero if agent state files exist before publish
- [ ] prepublishOnly script in package.json for all npm packages
- [ ] pre-commit hook blocking agent state files from commits
- [ ] Rotation cadence scheduled in shared calendar (quarterly minimum)
- [ ] Process documented for "engineer leaves the team" key rotation
### Ongoing
- [ ] New repo? Copy ignore list from template before first agent session
- [ ] New MCP server? Add to allow-list in code review, not informally
- [ ] Vendor incident? Rotate affected keys within 24 hours
What this means for your stack
The controls above are all things you can build yourself. They take time, and they drift. Ignore lists go stale when a new tool comes out. The shell wrapper breaks when someone upgrades their vault. The audit log is plain text with no integrity guarantee, so you can't use it in an incident report.
A local credential broker gives you the full version: vault encrypted with Argon2id, HMAC-chained audit log you can verify, broker handling 6 agent profiles without custom wrappers.
hasp is one working implementation. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, connect a project, and the next agent session gets a reference instead of a key. The audit log is verifiable with hasp audit --verify. Source-available (FCL-1.0), local-first, macOS and Linux, no account.
The architecture matters regardless of what you use to implement it. Agents that inherit ambient credentials produce incidents. Agents that request specific credentials through a broker produce audit logs. Where the credentials live before the session starts is what determines which outcome you get.
Sources· cited above, in one place
- Knostic Research on AI code editor secret leakage (Claude Code, Cursor)
- Snyk Security Labs MCP prompt-injection and supply-chain research
- OX Security AppSec research, including MCP ecosystem analysis
- GitGuardian State of Secrets Sprawl report
- HashiCorp Vault Documentation
- 1Password CLI op command-line tool
- Functional Source License FCL-1.0 text
Stop handing the agent your real keys.
hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.
- Local, encrypted vault — no account, no cloud, no telemetry by default.
- Brokered run — agent gets a reference, the child process gets the value.
- Pre-commit + pre-push hooks catch managed values before they ship.
- Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.