one command, every language

There's no integration to write.

cendarum run decrypts an environment on your machine and hands the values to your program through the spawn syscall itself. No SDK, no config file, nothing written to disk.

zsh
$ cendarum run --env dev -- npm run dev Email: you@example.com Master password: Secret Key (SK1-…): > my-api@0.1.0 dev > vite VITE v7.0.4 ready in 412 ms Local: http://localhost:5173/
Cendarum prints nothing of its own on a successful run. After the prompts, the terminal belongs to your app.

why it works everywhere

Injection happens below the language.

Process creation hands the new process an environment block — a list of KEY=VALUE pairs. That's an operating-system concept, not a library, and every runtime reads from the same one.

Node process.env.X
Python os.environ["X"]
Go os.Getenv("X")
Ruby ENV["X"]
Rust std::env::var("X")
Java System.getenv("X")
PHP getenv("X")
shell / Make $X

There is exactly one injection path to build, and it serves every language at once — which is why we will never write a per-ecosystem adapter. Child processes inherit the block, so injecting at the top of a tree flows all the way down.

prove it

Nothing leaks into your shell.

The second command is the interesting one. Same variable, no cendarum run, empty line.

zsh
$ cendarum run --env dev -- sh -c 'echo $DB_PASSWORD' s3cr3t-smoke-value $ sh -c 'echo $DB_PASSWORD'

the rules

Two decisions worth knowing about.

The vault wins

Your real environment is inherited — PATH, HOME, everything — and Cendarum's managed keys are overlaid on top. For the keys it manages, the vault is the source of truth.

This is also why an existing dotenv call in your app stays harmless: those libraries default to "an existing environment variable wins", so they find our value and leave it alone.

precedence
$ API_URL=ambient cendarum run --env dev -- printenv API_URL https://api.example.test

Loader variables are skipped

Some names don't configure your program — they configure the machinery that starts it. A stored value for one of those could hijack every process launched under cendarum run, which matters because a teammate with a write grant can edit your environment.

So they're skipped, with a warning naming the key, and --only is the explicit per-key override. The denylist runs after scopes compose, so a personal LD_PRELOAD is skipped like a shared one.

denylist
$ cendarum run --env dev -- printenv LD_PRELOAD cendarum: 'LD_PRELOAD' excluded (loader denylist); pass --only LD_PRELOAD to inject deliberately

per-developer overrides

Override one key for yourself. The team keeps theirs.

Precedence is ambient, then shared, then personal — and personal wins only for you. The override is encrypted under a key derived from your own key pair; teammates, admins, and our server can't read it.

personal scope
$ printf 'API_URL=personal-value\nMY_TOKEN=personal-tok\n' \ | cendarum import --env dev --scope personal cendarum: imported 2 personal overrides into dev (version 1) $ cendarum run --env dev -- printenv API_URL cendarum: personal overrides shadow the shared values of: API_URL — verify these are still current (a rotated team value stays shadowed by your override) personal-value
One value-free line names every shadowed key, on every run — a rotated team value stays shadowed by your override, and this line is the disclosure.

Deliberate friction

import and export require --scope, refused before any prompt or network call. There is no default to forget: a merged default is how a personal secret ends up inside a .env you hand to a teammate.

A read grant can write its own

Writing your own overlay mutates nothing shared, so a read grant is enough. The feature exists for exactly the read-only collaborator who needs a personal token — and nobody, including us, can read what they store.

The import wrote two keys, but the warning names only API_URL — a key that exists only in your overlay shadows nothing. Only real shadows are named, key names only, never values. Personal overrides, in detail

the other two commands

One to get in. One we'd rather you didn't need.

cendarum import

Your existing .env, encrypted, in one command. Comments, quotes, and export prefixes tolerated. Two lines setting the same key is an error naming both line numbers — never a silent last-one-wins.

Writes are scoped: --scope shared for the team's stream, --scope personal for an overlay only you can read. Required, not defaulted — a default would decide where your secrets land.

Reference

cendarum export

Writes plaintext to disk, for the rare tool that reads only a file. It warns on every invocation, there is no flag to silence it, and it refuses to write inside a git working tree without --force.

--scope shared|personal|effective is required; effective writes exactly what run would inject, shadow warning included.

Why the friction is the point

a use case people keep asking about

Let an AI agent make real API calls.

You want Claude Code or aider to actually hit the API — a real request, a real error you can debug. So it needs a real key. Paste it into the chat and the model refuses, correctly: a credential in a transcript is a credential sent to a provider.

zsh
$ cendarum run --env dev --only STRIPE_SECRET_KEY -- claude Email: you@example.com Master password: Secret Key (SK1-…): the agent now has one key, by name
The agent then writes $STRIPE_SECRET_KEY and your shell resolves it. The value is never in the conversation.

Referenced, not pasted

The agent inherits the environment, so it refers to the key by name. What lands in the transcript is $STRIPE_SECRET_KEY — not the value. Nothing to refuse, because nothing was pasted.

One key, not your whole environment

--only narrows what gets injected. An agent session is a long-running process doing things you did not approve one by one; it does not also need your database URL.

It is not a boundary against the agent — it runs in your environment, and curl -v prints the header. The docs say so plainly, along with what to do if it leaks. Read the whole pattern, limits included.

The honest limitation.

Injection has one exposure, and it's inherent rather than incidental.

An injected secret is visible to your own user account while the process runs.
ps e and /proc/<pid>/environ expose it. That's true of every tool that does environment injection. What it buys is the trade from "in a file on disk indefinitely, copied to six machines, one git add -A from being published" to "in one process, while it runs."
It's a snapshot, not a subscription.
Values are injected at spawn. Rotating a key, revoking a grant, or locking your vault does not reach into a process that is already running. Restart it.
It doesn't work in CI, deliberately.
A human types the master password; a build runner has nobody. Supporting it properly needs a machine identity — a service-account key pair granted access like any other member — which is a load-bearing change to the key hierarchy rather than a flag. There is no environment variable for the master password, for the same reason.

Try it on the project you're annoyed about.