Least Privilege for an Autonomous Agent

Most agent frameworks dump every secret into every code path and hope nothing leaks. aeon’s July rewrite moved to secretcurl: each skill declares the keys it needs, only those keys are injected, only for the duration of the call, only into the skill that asked.

PUBLISHED READ 5 MINFILED UNDER SECURITY / SECRETS / SKILLS / LEAST-PRIVILEGE

Most agent frameworks treat secrets the way a fresh dev treats a .env file. Dump them all in, let every code path see everything, and hope nothing leaks. It works right up until it doesn’t.

aeon’s July rewrite moved to a different model. Each skill declares the keys it needs. Only those keys are injected. Only for the duration of the call. Only into the skill that asked. The name of the pattern is secretcurl, and it’s one of the least glamorous changes in the last two months while also being one of the most consequential.

This article walks the before, the after, and the reason the shift is worth explaining in its own post.

§ 011. The old pattern: prefetch

The old aeon idiom for auth-heavy skills was called prefetch. A skill that needed, say, a paid API’s data would ship with a companion file: scripts/prefetch-<skillname>.sh. That script ran before Claude started, in a workflow step that had full access to every secret in the repo. It made the authenticated request, saved the response to a folder like .xai-cache/, and exited. Then Claude ran the skill, read the cached response from disk, and did the actual work - writing the brief, generating the digest.

The pattern existed for a reason. The Claude Code sandbox blocks env var expansion in shell blocks. A curl -H "Authorization: Bearer $XAI_KEY" inside a skill would fail - the sandbox refuses to interpolate the secret into the shell string. Prefetch was the workaround: do the authenticated request outside the sandbox, cache the result, let Claude work from the cache.

It worked. It was also a diffuse blast radius.

Every prefetch script ran with access to every secret in the repo. If any of them accidentally logged an environment variable, or shipped a wget of an attacker-controlled URL, or piped env into a base64 encoder, every credential the fork had was in scope. There was no per-skill wall.

§ 022. The failure mode nobody wants to demo

Imagine a community skill that ships with a well-meaning prefetch script. It hits a legitimate API, saves the response, exits cleanly. Six months later the upstream maintainer’s account gets compromised, and a small change lands in the prefetch script: a single curl -X POST call to an attacker’s endpoint, sending the output of env.

The change passes a normal code review. The diff is one line, the URL is obfuscated with a printf, and every fork that pulls the update executes it.

Every fork loses everything. Not just the key the compromised skill was supposed to use - everything.

This is a supply-chain risk any framework with community skills has to answer. aeon’s answer is to shrink the blast radius so far that the compromised-skill scenario steals one key, not the whole store.

§ 033. The new pattern: secretcurl

secretcurl is a small in-run authenticator that reads a skill’s declared secret list from aeon.yml and injects only those keys into the skill’s execution context. The skill still doesn’t get to interpolate them in shell blocks where the sandbox rule stands - but secretcurl can, on the skill’s behalf, and only during that one call.

The declaration in aeon.yml looks something like this:

skills:
  - name: crypto-scout
    secrets: [XAI_KEY, COINGECKO_KEY]

Two keys, not everything. If the skill file itself tries to reference STRIPE_KEY - a secret aeon has, but that this skill didn’t declare - the call fails. There’s no way for a skill to reach a key it didn’t ask for.

The mechanics of how this plays with the sandbox are worth naming. When the skill needs an authenticated request, it invokes ./secretcurl <alias>, passing the request shape. The wrapper reads the declared secret from an ephemeral scope that only that skill sees, executes the outbound call, and returns the response body. The secret never appears in the shell environment. The wrapper’s own memory is the smallest scope the credential ever lives in.

§ 044. Retiring the prefetch scripts

The migration is skill by skill. Each prefetch script is removed once its skill has a secretcurl invocation covering the same call. The cache folders (.xai-cache/ and the like) get deleted alongside it.

The observable difference in the Actions log: a prefetched run has a “Prefetch scripts” step running before Claude, with a batch of shell it does independently. A secretcurl run has that step gone. Every network call happens inside Claude’s execution, mediated by the wrapper, one skill at a time.

There’s a small performance hit - you don’t get to batch every skill’s prefetches in a single pre-run - and a large security win. On any run that involves a compromised or malicious skill, the credentials that skill could reach are exactly the ones it declared. Nothing more.

§ 055. Why this doesn’t need a framework rewrite

The important detail is that secretcurl is a wrapper, not a runtime change. Skills still run inside Claude Code. GitHub Actions is still the executor. The dashboard still shells out to gh. The change is a scoped credential injector in the middle of the call graph, added without disrupting anything upstream or downstream of it.

That fits aeon’s design idiom. When a security posture needs to shift, the mechanism is a new file in the toolchain, not a new abstraction layer. secretcurl sits where the network call already happened. The rest of the framework doesn’t need to know about it.

§ 066. What this pairs with

Two other July changes make this posture credible.

Postprocess retirement

The old pattern for irreversible sends - Telegram messages, tweets, paid calls - was to queue a request JSON to .pending-*/ during the run and let a postprocess-*.sh step drain the queue afterward. Same weakness as prefetch: the postprocess step ran with full env access. Now those sends happen in-run via secretcurl. The .pending-*/ folders are gone.

Signed outputs

When the output of a skill run is attested via Sigstore, the workflow identity that signed it is bound to the commit and the workflow file. Combine that with per-skill secret scoping and you can, for the first time, hand someone an output and say: the skill that produced this only had these keys, and here’s the signature to prove which workflow ran.

That’s not a demo. That’s a posture.

§ 077. The bar this sets

Least privilege isn’t novel in infrastructure. It’s the default in every well-run production system. The novelty is applying it inside an agent framework, where the ambient behavior for most tools is still “give the LLM every environment variable you have and hope it doesn’t accidentally curl them somewhere.”

aeon’s July rewrite chose the other default: skills declare what they need, and nothing else is in scope. When a compromise happens, the blast radius is a single key.

If you’re comparing agent frameworks and secrets management doesn’t come up in the conversation, that’s the framework whose demo will look great right up until a skill goes bad. secretcurl is aeon’s answer for what happens on that day.