CLAUDE LABJP
CODE — Claude Code adds Trusted Devices, verifying a machine before remote admin sessions beginCODE — CPU use drops about 37% during streaming, keeping long always-on automation steadierCODE — Fullscreen mouse-click controls, voice dictation fixes, and better Linux voice detection landAUTH — Static API keys can now be replaced with short-lived, scoped WIF credentialsTEAM — You can tag Claude directly in Slack and delegate tasks while you focus elsewhereWORKFLOW — Dynamic workflows arrive in research preview, breaking complex work into steps on their ownCODE — Claude Code adds Trusted Devices, verifying a machine before remote admin sessions beginCODE — CPU use drops about 37% during streaming, keeping long always-on automation steadierCODE — Fullscreen mouse-click controls, voice dictation fixes, and better Linux voice detection landAUTH — Static API keys can now be replaced with short-lived, scoped WIF credentialsTEAM — You can tag Claude directly in Slack and delegate tasks while you focus elsewhereWORKFLOW — Dynamic workflows arrive in research preview, breaking complex work into steps on their own
Articles/Claude Code
Claude Code/2026-06-28Intermediate

Stop Leaving a Static API Key in Your Unattended Jobs — Move to Short-Lived WIF Credentials

Claude Code is moving from static API keys toward short-lived, scoped credentials via WIF (Workload Identity Federation). Here is how to translate that idea to a small unattended pipeline so a leaked key has a much smaller blast radius — with working code and the failure modes to watch.

Claude Code170WIFSecurity4API KeysAutomation30

At 2 a.m., when no one is watching, a scheduled job wakes up, calls the API, finishes its work, and goes back to sleep. That stretch of "running unattended" is the part I find myself worrying about most. As an indie developer, I keep several sites updating themselves on staggered schedules, and honestly, the flashy new features matter less to me than one quiet question: is the API key I left sitting there going to leak someday without my noticing?

In its June 28, 2026 update, Claude Code signaled a move from static API keys toward short-lived, scoped credentials issued through WIF (Workload Identity Federation). It works with an OIDC-compliant identity provider, handing out throwaway credentials at request time. This article translates that idea down to a small, one-person, unattended pipeline: what you move, and how the blast radius shrinks when you do.

The scary part of a static key isn't the leak — it's the window

Putting ANTHROPIC_API_KEY in an environment variable or a .env file and reading it from cron works fine, and it's enough to start with. The problem is that the key stays valid forever.

The real danger of a static key isn't the moment it leaks. It's the window between the leak and the day you find out. It got printed into a CI log by accident, it rode along in a backup, it lingered in the shell history of an old machine — and in every one of those cases, you usually discover it weeks later. For all that time, a fully scoped, valid key has been walking around outside.

Unattended operation sharpens this. No one is staring at a dashboard, so a sudden spike of suspicious requests goes unnoticed until well after the fact. That's exactly why two properties — "short-lived even if leaked" and "narrowly scoped even if leaked" — change how safe the whole setup feels.

WIF doesn't hand out keys — it trades for them on the spot

The idea behind WIF, in one line: stop distributing a long-lived secret (the static key), and instead trade a trustworthy proof of identity for a short-lived credential, every time you need one.

The flow is roughly three steps. First, the runtime obtains a token proving its identity (an ID token) from an OIDC-compliant identity provider. Second, it presents that token to a credential issuer and receives a short-lived, narrowly scoped credential in return. Third, it calls the actual API with that short-lived credential. Once the job ends and the credential expires, it's worthless to anyone.

Lining up static keys against short-lived credentials makes the operational meaning clear.

AspectStatic API keyWIF short-lived credential
LifetimeValid until you revoke itAuto-expires in minutes to ~1 hour
StorageEnv var, .env, secret storeIn memory only; never persisted
Impact if leakedFull-scope abuse until noticedUseless after expiry; scope-bound
RotationManual or a periodic jobEffectively rotated every request
Proof of identityPossessing the key = youOIDC token verifies the runtime

The bottom-right cell is the point. With a static key, possession is identity, so a copy is game over. WIF verifies which runtime the request came from every time, so lifting the key string alone gets an attacker nowhere without an environment that passes verification.

The smallest migration unit: turn "inline key" into "fetch function"

You don't have to move everything to WIF at once. The smallest possible step is to replace every spot that reads the API key directly with a call to a function that fetches a credential. Hide that behind a function, and you can swap the insides from a static key to a short-lived credential later without touching any caller.

The code below packs that fetch function into a single class. It trades an OIDC token for a short-lived credential, caches it in memory until expiry approaches, and refreshes proactively just before the deadline. Endpoints and field names vary by environment and provider, so fill them in while checking the real field names in the official docs.

import os
import time
import threading
import requests
 
class ShortLivedCredentialProvider:
    """Trade an OIDC token for a short-lived credential; refresh before expiry.
 
    Callers only call get(). Whether the value is a static key or a WIF
    credential, the interface stays the same.
    """
 
    # How many seconds before expiry to stop using it (clock skew + latency margin)
    REFRESH_BUFFER_SEC = 120
 
    def __init__(self, token_url: str, exchange_url: str, audience: str):
        self._token_url = token_url        # where to get the OIDC ID token
        self._exchange_url = exchange_url  # where to exchange for a short-lived credential
        self._audience = audience          # audience the exchanger expects
        self._lock = threading.Lock()
        self._cached_value = None
        self._expires_at = 0.0
 
    def _fetch_id_token(self) -> str:
        # Obtain an OIDC ID token from the runtime's metadata service, etc.
        # The retrieval method differs per cloud/CI, so adapt this part.
        resp = requests.get(
            self._token_url,
            params={"audience": self._audience},
            headers={"Metadata-Flavor": "Google"},  # example; varies by provider
            timeout=10,
        )
        resp.raise_for_status()
        return resp.text.strip()
 
    def _exchange(self, id_token: str) -> tuple[str, float]:
        # Present the ID token and receive a short-lived, scoped credential.
        resp = requests.post(
            self._exchange_url,
            json={
                "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
                "subject_token": id_token,
                "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
                "scope": "messages:write",  # request only the minimum scope
            },
            timeout=10,
        )
        resp.raise_for_status()
        data = resp.json()
        # Map field names to your provider's spec
        value = data["access_token"]
        ttl = float(data.get("expires_in", 900))
        return value, time.time() + ttl
 
    def get(self) -> str:
        now = time.time()
        # If we're past the pre-expiry mark, take the lock and refresh
        if now >= self._expires_at - self.REFRESH_BUFFER_SEC:
            with self._lock:
                # Another thread may have refreshed while we waited for the lock
                if now >= self._expires_at - self.REFRESH_BUFFER_SEC:
                    id_token = self._fetch_id_token()
                    self._cached_value, self._expires_at = self._exchange(id_token)
        return self._cached_value
 
 
# Callers do only this. They never need to know where the key comes from.
provider = ShortLivedCredentialProvider(
    token_url=os.environ["OIDC_TOKEN_URL"],
    exchange_url=os.environ["CREDENTIAL_EXCHANGE_URL"],
    audience=os.environ["EXCHANGE_AUDIENCE"],
)
 
# Hand the Anthropic client a freshly minted credential each time
from anthropic import Anthropic
 
def make_client() -> Anthropic:
    return Anthropic(api_key=provider.get())

The quiet hero here is REFRESH_BUFFER_SEC. Instead of using a credential right up to its expiry, you stop using it a little before. In a long batch, there's a gap between fetching a credential and the call actually reaching the server, plus clock drift between the two machines. Pushing to the exact second occasionally produces "valid by my clock, expired by theirs." Cutting off early is a structural guard against that boundary failure.

The three usual stumbles: clock, scope, fallback

When you actually move things over, the trip-ups are fairly predictable.

First, clock drift. Because short-lived credentials live for so little time, even a small time difference with the server surfaces. On top of the buffer above, confirm once that NTP sync is working on the runtime, and you'll quietly avoid a class of mysterious 401s.

Second, over-broad scope. If you go to the trouble of making credentials short-lived but still mint full-scope ones every time, you've thrown away half the point. Put only the operations the job truly needs into scope. A job that only posts an article shouldn't carry billing or admin permissions. Short-lived and narrow are two different defenses; they only really pay off together.

Third, the direction of your fallback. If you leave an escape hatch that says "on failure, just run with the old static key," you've kept a long-lived, full-scope key alive after all. Here, don't overthink it — fail closed, deny by default. For unattended operation, that's safer than staying up. Prioritize not widening the damage over staying alive. The idea of shrinking the secret-exposure surface itself also comes up in The sandbox can run code but can't read your auth files.

Start with one job, hidden behind the fetch function

Rather than aiming for a full cutover, pick the single job that would hurt least if it broke, and hide its credential behind the fetch function there. Once you confirm the calling code didn't change, you can fold the rest of your jobs in one by one, just by sharing the same function.

And if you've ever committed a key to history in the past, doing an inventory alongside the migration — see Emergency steps when you've accidentally committed an API key — lets you revoke the old static key with confidence. Moving to short-lived credentials makes that "throw the old key away" decision much lighter.

If you're running something unattended too, I hope this helps. Thanks for reading.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-06-27
Will It Stay Light When You Run It Unattended? Observing and Capping Claude Code's Long-Session Memory
How to keep long, unattended Claude Code sessions from slowly getting heavier — with a tiny ps-based RSS sampler, a rolling-baseline watchdog, and session segmentation, shown with working scripts and a before/after comparison.
Claude Code2026-06-25
Higher Rate Limits Don't Mean Tighter Schedules — Spend the Headroom on 429 Recovery
When Claude Code's rate limits went up, the instinct was to pack the schedule tighter. Here's why I did the opposite and routed the new headroom into retry budget instead — a pacing note for unattended pipelines.
Claude Code2026-06-19
An Article My Gate Rejected Got Published — The Cost of Chaining the Quality Gate and git push in One Call
In an unattended publishing pipeline, an article my quality gate had rejected went live anyway. The cause was chaining the gate and git push into a single shell call. Here is how the exit code gets swallowed, and a two-phase publish-marker design that refuses to push until every gate has demonstrably passed.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →