An Agent Cannot Work Autonomously If It Keeps Asking for Credentials
An agent that stops to ask for credentials is not autonomous. It is a remote-control session with a very expensive operator.
One approval during a task is a speed bump. Asking repeatedly in the same session breaks the loop completely: the agent cannot inspect a job, try a fix, verify it, and continue while a human is being summoned for every command.
That was exactly what happened with Nomad in Claude Code. I had a token cache that worked perfectly in tmux, but nomad job status still asked for Touch ID. Again. And again. Every single command.
Why was an apparently cached credential still blocking the agent? The answer was two separate boundaries that looked like one annoying prompt:
- Claude Code starts a fresh shell for each Bash invocation, so a token exported by one command is gone for the next.
- The workaround put
NOMAD_TOKEN=$(op read ...)in front of every command, which stopped the command from matching Claude Code’s existingBash(nomad job:*)permission rule.
The result was particularly stupid: Touch ID for 1Password and an agent permission approval for a command that was already supposed to be allowed. Instead of executing a short diagnostic sequence, the agent was waiting for me after every step.
The fix was not more permission rules. It was moving the cache to the one place that survives shell processes and is designed to hold credentials: the macOS login keychain. The agent can then use an ordinary nomad command; I only need to approve an initial read after a token rotation.
Autonomy fails at the process boundary
My old wrapper wrote the token to tmux’s global environment. That is a reasonable trick for an interactive terminal session: every pane can inherit the value, and the token disappears when the tmux server does.
But Claude Code’s Bash tool does not run inside that environment. Each tool call is a new shell with no $TMUX, so there is no shared process-level state to retrieve.
The cache was fine. Its scope was wrong. It helped an interactive human session, not an agent whose work is split across short-lived processes.
The credential workaround that created a second blocker
The first workaround was to acquire the token inline:
NOMAD_TOKEN=$(op read "op://Private/Nomad Management Token/credential" --account my.1password.com) nomad job run service.hcl
That removes the missing-token problem, but creates another one. Permission matching begins with the first token. NOMAD_TOKEN=... is not nomad, so the command no longer matches an allow rule such as Bash(nomad job:*).
This is a useful general rule for coding-agent permissions: keep the executable command plain. Put credential acquisition behind a stable wrapper, rather than changing every invocation’s first token. Otherwise, a credential workaround becomes another interruption between an agent and the next useful action.
Cache once, then let the agent finish the job
The wrapper now checks the macOS login keychain first. On a cache miss, it reads the token from 1Password once, stores it in the keychain, exports it for the current shell, and then invokes the real Nomad binary.
NOMAD_TOKEN_KEYCHAIN_SERVICE="nomad-token"
NOMAD_TOKEN_OP_REF="op://Private/Nomad Management Token/credential"
nomad-token() {
local token
token=$(security find-generic-password \
-s "$NOMAD_TOKEN_KEYCHAIN_SERVICE" -a "$USER" -w 2>/dev/null)
if [[ -z "$token" ]]; then
token=$(op read "$NOMAD_TOKEN_OP_REF" --account my.1password.com) || return 1
[[ -n "$token" ]] || return 1
printf '%s\n%s\n' "$token" "$token" |
security add-generic-password -U \
-s "$NOMAD_TOKEN_KEYCHAIN_SERVICE" \
-a "$USER" \
-l "Nomad Management Token cached from 1Password" \
-A -w >/dev/null 2>&1 || return 1
fi
print -r -- "$token"
}
nomad() {
if [[ -z "$NOMAD_TOKEN" ]]; then
local token
token=$(nomad-token) || return 1
export NOMAD_TOKEN="$token"
fi
command nomad "$@"
}
Once seeded, both a human shell and a new Claude Code shell can run a plain command:
nomad job status
nomad job run service.hcl
The keychain is encrypted at rest and unlocked as part of the user login session. That makes it a better fit than plaintext agent settings or an .env file, while retaining the familiar 1Password source of truth for the initial read.
The practical change is bigger than a faster command. A coding agent can now make a sequence of safe, already-authorized Nomad calls—inspect, deploy, check status, collect evidence—without turning each step into a request for human presence.
The sharp edge: -A is a deliberate trade-off
The important flag is -A. It makes the cached keychain item readable without another per-access prompt. Without it, macOS may prompt even though the item is in the login keychain.
That also means any process running as my macOS user can read this item. This is not the strongest possible access-control model; it is a conscious usability trade-off for a non-expiring Nomad management token on my own machine.
It is still materially better than leaving a root-equivalent token in a plaintext dotfile or agent configuration. But do not copy this blindly to a shared account, a multi-user host, or a token with a larger blast radius than you are willing to expose to your user session.
There is another small but easy-to-miss detail: security add-generic-password -w consumes the next argument as the password. If the next token is another option, it stores that option literally. Leaving -w last makes it read the password and confirmation from standard input, which is why the wrapper feeds the token twice. It keeps the secret out of the process argument list.
Rotation is cache invalidation, honestly
Nomad management tokens do not expire in this setup, so a TTL just reintroduces scheduled Touch ID prompts without improving anything. Detect-and-retry is worse: an authorization failure is not necessarily a bad token, and retrying deploy commands can be dangerous.
The boring answer wins: clear the cache after rotating the token.
nomad-token-refresh() {
security delete-generic-password \
-s "$NOMAD_TOKEN_KEYCHAIN_SERVICE" -a "$USER" >/dev/null 2>&1
nomad-token >/dev/null && echo "Nomad token re-seeded from 1Password"
}
This makes rotation explicit, predictable, and easy to document.
The revised mental model: credentials are part of agent UX
1Password was never the bottleneck. The credential was being requested repeatedly because the cache lived in the wrong process boundary, and the workaround accidentally defeated the agent’s command permission model.
For credentials used by short-lived local processes, choose storage that matches the process lifetime: the OS keychain, not a shell or tmux environment. Then keep the actual command stable enough for policy tooling to recognize it.
The security boundary remains deliberate: a human authorizes the first 1Password read, the keychain protects the cached value at rest, and the agent only gets the capability needed by the existing nomad wrapper. But repeated interactive friction is removed.
That turns the workflow back into what an agentic system should be: one Touch ID prompt after a rotation, then an agent that can complete the entire task instead of asking for help at every command.