Every app has two kinds of settings. There is ordinary config, a log level, a feature flag, a public URL, stuff you would not mind reading over your shoulder. And there are secrets, an API key, a database password, values that must never leak. Burrow keeps a clean line between the two and gives you simple commands for each. This is the practical guide: how to set them, update them, list them, and control exactly when a change takes effect.
Config: the settings you do not need to hide
Ordinary config goes in with a plain key and value:
burrow app config set web LOG_LEVEL=info
burrow app config set web FEATURE_NEW_CHECKOUT=true
Your app reads these as environment variables, the same way it reads config anywhere. To see what an app currently has set:
burrow app config web
That lists the config keys and their values, because these are not sensitive, that is fine. To remove one:
burrow app config unset web FEATURE_NEW_CHECKOUT
By default, setting or unsetting config rolls the running app so it picks up the change right away. That is usually what you want: change the value, the app restarts cleanly onto the new value, done. When you would rather batch the change and let it land on your next deploy instead of restarting now, add --no-restart:
burrow app config set web LOG_LEVEL=debug --no-restart
The value is persisted immediately, but the running app keeps going undisturbed until the next deploy carries it in. Handy when you are lining up several changes and do not want a restart per edit.
Secrets: the settings you must hide
Secrets use a parallel set of commands, and the difference in how they behave is the whole point. Set one at your own terminal:
burrow app secret set web STRIPE_SECRET_KEY=sk_live_...
burrow app secret set web DATABASE_PASSWORD=...
Your app reads these as environment variables too, right alongside your config, so from the app's point of view there is no difference in how you use them. The difference is entirely in how Burrow treats the value. A secret value goes straight to Burrow over an authenticated connection, gets written into the app's own protected storage, and is never logged and never kept in Burrow's own database in the clear.
Listing secrets shows you the keys, never the values:
burrow app secret list web
You get back STRIPE_SECRET_KEY, DATABASE_PASSWORD, the names, so you can confirm what is set without ever exposing what it is. To remove one:
burrow app secret unset web DATABASE_PASSWORD
Like config, secret changes roll the app by default so it picks them up, and --no-restart defers that to the next deploy.
The one rule worth internalizing: set secrets yourself
Here is the habit that keeps you safe. Set secret values yourself, at your own terminal, never by pasting them into an agent's chat.
The reason is concrete. A value pasted into an agent conversation is retained in that conversation and sent again on later messages, which is exactly the wrong place for a live API key to live. So Burrow is built so you never have to. There is no "set a secret value" command in the agent's command surface at all. The agent can confirm a key exists and wire your app to use it, but it cannot read or set the value. You handle the value, once, in your own terminal, and everything downstream just works.
This is why config and secrets are two separate commands instead of one. It is not bureaucracy, it is the line between "the agent can manage this" and "only you touch the value."
Which is which: a quick test
Sometimes it is not obvious whether a value is config or a secret. A simple test settles most cases: would you mind this value showing up in a log, a screenshot, or a support chat? If you would not care, it is config. If the thought makes you wince, it is a secret.
A log level, a feature flag, a public API base URL, the name of a region: nobody is harmed if these are visible, so they are config, and it is fine for them to show their values and for an agent to manage them. An API key, a database password, a signing secret, a token: these cause real damage if they leak, so they are secrets, and they get the treatment that keeps their values out of logs, out of Burrow's database, and out of any agent conversation.
When in doubt, treat it as a secret. The cost of putting a not sensitive value in as a secret is small, you just do not get to read it back later. The cost of putting a sensitive value in as plain config is a leak. So the safe direction to round is toward secret.
Rotating a secret
Secrets do not last forever. Keys get rotated, passwords get changed, tokens expire. Rotating one with Burrow is just setting the same key to a new value:
burrow app secret set web STRIPE_SECRET_KEY=sk_live_newvalue...
Because setting a secret rolls the app by default, the new value takes effect right away: the app restarts cleanly onto the new key. If you are coordinating a rotation where the old key needs to keep working until the new one is fully in place, use --no-restart to stage the new value and let it land on your next deploy, so you control the exact moment of the switch.
The thing rotation should never involve is a value passing through an agent. Rotate secrets yourself, at your terminal, the same way you set them the first time. The agent can confirm afterward that the key is still present with burrow app secret list web, which is all it needs to know to keep your app wired up correctly.
Config from addons: the ones you do not set by hand
Some config lands automatically, and it is worth knowing so you are not confused when a value appears. When you attach a Postgres database to an app, Burrow wires the connection in for you as a DATABASE_URL, without you setting anything:
burrow addon attach postgres web
After that, your app has DATABASE_URL in its environment, populated with the right credentials for its own database. You did not run secret set for it, and you should not: it is managed for you, and it stays correct as things change. So if you list your app's environment and see values you do not remember setting, an attached addon probably put them there on purpose.
Promoting settings across environments
Because config and secrets are per app and your environments are separate, staging and prod each carry their own values. That is what you want: your staging Stripe key is the test key, your prod one is live, and they never get crossed.
The workflow is to set each environment's values against that environment. Point at staging, set staging's values, point at prod, set prod's:
burrow env use staging
burrow app secret set web STRIPE_SECRET_KEY=sk_test_...
burrow env use prod
burrow app secret set web STRIPE_SECRET_KEY=sk_live_...
The same app name, the same key name, a different value in each place, kept apart by the environment boundary. There is no accidental sharing, because there is no shared store to leak across.
How your app actually reads these
A question that comes up early: how does my code see any of this? The answer is deliberately boring. Both config and secrets arrive in your app as environment variables, the same channel your code already reads for PORT or NODE_ENV. There is nothing Burrow specific to import, no SDK to add, no special file to parse. If your framework reads process.env.STRIPE_SECRET_KEY, os.environ["DATABASE_PASSWORD"], or os.Getenv("LOG_LEVEL"), it just works.
That is on purpose. The whole design goal is that your app should not know or care whether a value came in as plain config or as a hidden secret. The distinction lives entirely on the Burrow side, in how the value is stored, listed, and logged. From inside the container the two are indistinguishable, which is exactly what you want: your code stays portable and you can move a value from config to secret later without touching a line of it.
One consequence worth holding onto: because these are environment variables, they are read once when the process starts. That is why a change rolls the app by default. The restart is not ceremony, it is the only moment your process reads its environment fresh. If you skip the restart with --no-restart, the running process keeps the old value in memory until it next starts, which is precisely the batching behavior you asked for.
Putting it together
The mental model is small and worth holding onto:
- Config is for values you do not need to hide. Set them with
burrow app config set, list them withburrow app config web, and they show their values. An agent can manage these. - Secrets are for values you must hide. Set them yourself with
burrow app secret set, list only the keys withburrow app secret list web, and the values never surface, never get logged, and never go through an agent. - Restarts happen by default so changes take effect now. Use
--no-restartto batch a change onto the next deploy. - Addon config like
DATABASE_URLis wired in for you when you attach a backing service. Do not set it by hand.
A note on restarts and timing
One more practical point, because it catches people. By default, changing config or a secret rolls the app, which means a brief, clean restart onto the new value. For a single change that is exactly what you want. But if you are making several changes in a row, each one restarting the app is wasteful and a little disruptive.
The move there is to make all but the last change with --no-restart, so they are persisted quietly, then let the final change, or your next deploy, carry them all in at once:
burrow app config set web FEATURE_A=true --no-restart
burrow app config set web FEATURE_B=true --no-restart
burrow app config set web FEATURE_C=true # this one rolls, and brings A and B with it
Or persist everything with --no-restart and let your next deploy apply the whole batch. Either way you get one restart instead of three, and you control exactly when the app picks up the new values. Small thing, but it keeps a flurry of config edits from turning into a flurry of restarts.
What can go wrong, and how to tell
A few situations come up often enough to name, so you recognize them instead of debugging from scratch.
The app did not pick up a new value. Almost always this is a --no-restart left on, or a change staged for the next deploy that has not happened yet. The value is saved, the running process just has not read it. Deploy, or set it again without --no-restart, and the restart carries it in. When in doubt, burrow app config list web confirms the stored value, and if that looks right but the app disagrees, the process has not restarted onto it.
A key seems to have vanished. Listing config shows only config, and listing secrets shows only secret keys. If you set API_TOKEN as a secret and then look for it in burrow app config list web, it will not be there, because it lives on the secret side. Check the other list. This is the storage boundary doing its job, not a lost value.
A value shows up that you did not set. That is almost certainly an attached addon. DATABASE_URL from Postgres, a cache connection string, and similar wired in values are managed for you. Leave them alone: setting them by hand fights the addon and drifts out of sync the next time credentials rotate.
You typed a secret into the agent chat by reflex. If it happens, treat that value as compromised: rotate it. Set a fresh value at your own terminal with burrow app secret set web KEY, and the old one stops mattering. This is exactly why the agent surface has no command to set a secret value: the tooling steers you away from the mistake, but the recovery is a one line rotation either way.
A short FAQ
Can the agent read my secrets? No. It can confirm a key exists with the read only listing, which returns names and never values, and it can wire your app to use a secret. It cannot read or set the value. The value only ever moves between your terminal and Burrow's own API, never over the agent channel and never into a log.
Does deploy take config? No, and that is intentional. Config and secrets have their own lifecycle, set once and updated on their own schedule, independent of shipping a new image. A deploy carries code, not settings. This keeps the two concerns from tangling: you can roll out new code without touching config, and change config without a new build.
Can I use the same key name in staging and prod? Yes, and you should. Values are per app and per environment, so STRIPE_SECRET_KEY can be the test key in staging and the live key in prod with no risk of crossing, because there is no shared store between environments to leak across.
What about credentials for a connected back end or vendor? They flow the same private way as any secret: set at your terminal, stored in the app's protected storage, never logged and never over the agent channel. There is no separate, lesser path for third party credentials.
That is the whole system. Two commands, a clean line between them, and a firm rule that the sensitive values stay in your hands. Config you can share, secrets you keep, and the one who sets the value is always you.