This is part 8 of the series "dotvault, Start to Finish: One Command from Your Laptop to CI, Your Team, and Your AI Agents." dotvault will be released free of charge when this series ends.
I gave Claude Code exactly one thing: a dotvault service token, limited to one project and one environment. No .env in the chat, no email and password. It ran dotvault init, import, pull, run and whoami on its own, imported 3 keys and pulled the same 3 back, then wrote me a report of what it had done. When I revoked the token in the console and asked for the same pull again, it failed with AUTH_REQUIRED and exit code 3, and Claude Code told me so.
This is part 8 of 12 in a series on dotvault, which keeps .env secrets for a team and hands them to people, CI and AI agents. I also checked that dotvault scan . flags the token in .claude/settings.local.json even though that file is in .gitignore.
The short version
- Claude Code gets one token. In the console, Service Tokens → Create token, with Role Read/write and Project set to the project it works on. Read/write can pull and can add, update and delete keys.
- A person creates the project. A Read/write token cannot create projects, so you create it in the console first (part 2 shows how).
-
The token goes in the repository's
.claude/settings.local.json, underenv, asDOTVAULT_TOKEN. It sits there as plain text, so list the file in.gitignore. -
Claude Code imports and receives the values with no human login. It ran
init→import→pull→run→whoamiitself: 3 keys imported, 3 keys pulled. -
dotvault scan .finds the token in the settings file. It reports the one token as 2 findings and exits 1. -
Revoke stops it. After Revoke, Claude Code's
pullfailed withAUTH_REQUIRED(exit 3). Swapping the token is revoke, issue a new one, replace one line. The API keys the agent uses are replaced by creating a new key at the provider and updating the value in dotvault; part 11 covers that.
Before you read on
- Where we are. Part 7 covered issuing and revoking a service token for GitHub Actions. Here the same kind of token goes to Claude Code.
-
Terminal screens are rebuilt. The Claude Code screens and terminal output are copied character for character and re-rendered for legibility. Recorded exit codes appear as
echo $?lines. -
Tokens are masked as
est_••••in screenshots and terminals, and the Last-use IP in the token list is masked too. The token created for this part was revoked afterwards. - Terms. "AI agent" here means Claude Code, which works by running commands in a terminal. "member" is dotvault's internal role name for a Read/write token.
1. One project, one token scoped to it
First, a person creates the project the agent will use. I created demo-agent-app from Projects → New project, the same way as in part 2. A Read/write token cannot create projects, so this step stays with a human.
Then Service Tokens → Create token: Name claude-agent, Role Read/write (member), Project demo-agent-app, Expiry 30 days, and only production ticked under Environments.
Read/write rather than Read only, because the agent has to import. The environment boxes do what the hint says: leave them empty and the token covers every environment; tick some and it covers only those. The agent only writes to production, so production is all it gets.
Create shows the token once. Copy it there.
The list now has one row: Project demo-agent-app, Envs production.
2. Put the token where Claude Code reads it
In the repository (~/dev/demo-agent-app here), write the token into .claude/settings.local.json. Claude Code passes this file's env to every command it runs.
{
"env": {
"DOTVAULT_TOKEN": "est_••••"
},
"permissions": {
"allow": [
"Bash(dotvault:*)"
]
}
}
-
DOTVAULT_TOKENholds the Read/write token.import,pullandrunread it. -
Bash(dotvault:*)inpermissions.allowlets Claude Code run commands that start withdotvaultwithout asking each time.
Why the repository file and not ~/.claude/settings.json: a token in your user-wide settings reaches Claude Code in every repository you open. This token belongs to one repository, so it lives there.
The file holds the token as plain text. Keep it and .env out of commits:
.env
.claude/settings.local.json
The first time Claude Code starts in this repository, the folder trust prompt lists what the file pre-approves.
Check that Bash(dotvault:*) is the only thing on that list, then pick Yes, I trust this folder.
3. Ask Claude Code to import and pull
The repository holds a .env with three dummy lines (DATABASE_URL, OPENAI_API_KEY, FEATURE_FLAGS). The request:
Move this repo's .env into dotvault without asking me. The project demo-agent-app already exists. Use the dotvault CLI: init the config as demo-agent-app, import .env into production, pull it back from production, then check two values with
dotvault run -E production -- printenv DATABASE_URLanddotvault run -E production -- printenv FEATURE_FLAGS(one name per command, never print the whole environment), and finish with whoami.
The parenthesis matters. dotvault run passes the values to the child process as environment variables, so a bare printenv would dump every one of them onto the screen and into the conversation log. One name per command, because macOS printenv prints only the first of two names:
$ env A=1 B=2 printenv A B
1
Claude Code ran 7 commands. The first 2 read dotvault --help and the subcommand help; the other 5 did the work. Here they are with their output, expanded with ctrl+o.
-
init wrote
project-config.jsonand said the next step needs no login: "uses DOTVAULT_TOKEN; no login needed". - import put 3 keys into production.
-
pull wrote the same 3 back. It also warned that it had commented out the three original lines in
.envand written the values into the block dotvault manages below them. - run printed the two dummy values, one per command.
- whoami reported a service token in CI / Lambda mode, not a person.
Then Claude Code reported back:
It lists what it ran and what passed, down to the key counts, and it mentions the commented-out lines on its own. I did not have to answer anything along the way.
4. Check in the console
In the console, demo-agent-app → Keys → production shows the 3 keys Claude Code imported.
5. dotvault scan finds the token
dotvault scan, from part 5, also catches est_ tokens. In the repository:
$ dotvault scan .
.claude/settings.local.json:3 [dotvault-service-token] dotvault service token — est_•••• (80 chars)
.claude/settings.local.json:3 [generic-assignment] Generic secret assignment — TOKE•••• (90 chars)
2 potential secret(s) found.
$ echo $?
1
One token, two rules (dotvault service token and Generic secret assignment), two findings. scan prints a few characters from each end of what it finds; I masked those too. The dummy values in .env were not reported.
The settings file is in .gitignore, but as part 5 showed, scan . reads ignored files too. If your pre-commit hook runs exec dotvault scan ., this file alone will block every commit; narrow the hook to something like exec dotvault scan src/ .github/. The other way round, if the token ever lands in a committed file such as a shared .claude/settings.json, the hook stops the commit.
6. Revoke stops the agent
In Service Tokens, I clicked Revoke on the claude-agent row and confirmed "Revoke token “claude-agent”? CI jobs and services using it will lose access immediately." The status changed to revoked.
Last used is recorded at most once an hour per token, so the time shown is the agent's first use of the token in section 3 (the command that ran init and import). After the revoke, I asked Claude Code for the same pull.
The pull stopped with AUTH_REQUIRED and exit code 3. Claude Code read it correctly: the token was revoked or expired, the local .env was untouched, and a new token would get it going again.
Do not use whoami to check this. As its own note in section 3 says, it does not contact the server, so a revoked token prints the same line. Use pull or dotvault list --project <name>.
Once the token is revoked, clear the DOTVAULT_TOKEN value in .claude/settings.local.json as well.
7. Reissuing the token and replacing keys stays easy
Reissuing the agent's token is three steps: revoke it, issue a new one, and replace the one line in .claude/settings.local.json. Nothing else in Claude Code changes.
The API keys the agent uses, like OPENAI_API_KEY here, are easy to replace too. dotvault does not create the key itself: you create a new key at the provider, such as OpenAI, and replace the value stored in dotvault. Register a key for rotation management and, if it leaks, retire it so no one receives it any more, then set a new value. Part 11 covers how that works.
Next
Part 9 moves from the Personal org used so far to a Company org, invites people, gives them roles, and shuts out someone who has left.
dotvault stores the secrets in your .env and config files server-side and hands them to any authorised developer, CI job or AI agent with a single command (https://dotvault.io). It will be released free of charge when this series ends, and the steps in these articles are the steps you will use on day one.
If you find this useful, please like and follow.
Our products are listed at https://www.eln.ne.jp/products
EarthLink Network builds its own products to run the whole company on AI — each one born from a need along the way. See the full index of what we build:
→ The 18 products EarthLink Network builds in-house
For more about the company and each product, visit www.eln.ne.jp.




![Claude Code's run, expanded with ctrl+o. Bash(dotvault init --name demo-agent-app && dotvault import .env -E production) prints "Created project-config.json (project=demo-agent-app). Next: dotvault pull (uses DOTVAULT_TOKEN; no login needed)" and "imported 3 keys into .env [demo-agent-app/production]". Bash(dotvault pull -E production) prints a warning that the three original lines were commented out, then "✓ Wrote 3 keys to …/demo-agent-app/.env" and "✔ 1 file synced, 0 excluded, 3 keys total". printenv DATABASE_URL prints a dummy postgres:// URL and printenv FEATURE_FLAGS prints beta-search. Bash(dotvault whoami) prints "Service token (DOTVAULT_TOKEN=est_••••…) — CI / Lambda mode" and a note that whoami does not contact the server.](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft3pa3bwx0qw3mk5x3x7d.png)




Top comments (0)