Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 7 min read

Pasting .env into Claude Code? Give the agent one dotvault service token instead

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

Pasting .env into Claude Code? Give the agent one dotvault service token instead

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, under env, as DOTVAULT_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 β†’ whoami itself: 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 pull failed with AUTH_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.

The Create service token dialog: Name claude-agent; Role Read/write, described as "In addition to pull, can add, update, and delete keys."; Project demo-agent-app, with the note "Project-scoped tokens receive only the Global keys bound to this project (they cannot see the Global keys list itself)."; Expiry 30 days; Environments (envs) with development, staging and production, only production ticked, and "Leave empty for all environments. Picking some restricts the token to them."; Cancel and Create.

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.

Token created, token masked: "Copy this token now β€” it is shown only once and cannot be retrieved again.", est_β€’β€’β€’β€’, Token ID 6db15215ff44cb01095ffd38cec39774 with "The Token ID is not a secret.", Copy and Done.

The list now has one row: Project demo-agent-app, Envs production.

Service Tokens with one row: claude-agent, Token ID 6db15215ff44cb01095ffd38cec39774, Role Read/write, Project demo-agent-app, Envs production, expiring 30 days after creation, Last used Not recorded, Status active, and a Revoke button.

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_TOKEN holds the Read/write token. import, pull and run read it.
  • Bash(dotvault:*) in permissions.allow lets Claude Code run commands that start with dotvault without 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.

Claude Code's start-up prompt: "Accessing workspace:" with the repository path, a question about whether you trust the folder, then in yellow "⚠ This folder pre-approves 1 tool permission in .claude/settings.local.json:" followed by Bash(dotvault:*) and "These will apply without asking. Only proceed if you trust this configuration."; options No, exit and Yes, I trust this folder.

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_URL and dotvault 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.

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.

  • init wrote project-config.json and 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 .env and 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:

Claude Code's report. Under the request, "Ran 7 shell commands", then "The repo's .env is now in dotvault under demo-agent-app, in the production environment." and five items: Init (project-config.json created), Import (3 keys into production), Pull (3 keys written back to .env, original lines commented out), Checks (the DATABASE_URL and FEATURE_FLAGS values), and whoami (a service token is in use; whoami does not contact the server, so to check the token directly run dotvault list --project demo-agent-app). "Churned for 24s" at the end.

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.

demo-agent-app's Keys tab with production selected: the env file with 3 keys, DATABASE_URL, FEATURE_FLAGS and OPENAI_API_KEY, each with a lock icon and the same UPDATED time, and a "Selected key" panel on the right.

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.

Service Tokens filtered to Revoked only and searched by Token ID: one row, claude-agent (Read/write, demo-agent-app, production), Last used (hourly) showing when the token was used in section 3, Last-use IP masked, Status 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.

Claude Code, expanded with ctrl+o. The request "Pull production from dotvault again with dotvault pull -E production and tell me the exit code." and Bash(dotvault pull -E production; echo "exit code: $?") printing "βœ– AUTH_REQUIRED: invalid, expired or revoked service token" and "exit code: 3". The report starts "The pull failed with exit code 3 (AUTH_REQUIRED).", says the token has probably been revoked or has expired, that the failed pull wrote nothing so the local .env is unchanged, and that a new token from the admin console set as DOTVAULT_TOKEN would let it re-run the 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.

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.