Skip to content

Keep local values in env files

View as Markdown

This guide keeps your pack's local settings and secrets in env files, and shows what reads each one under kindgi dev.

  • A pack with kindgi dev, from Kindgi 0.1's CLI. This guide uses the quickstart's pack, my-pack.
  • A one-step flow that calls the tool, to try it. This guide uses my-pack.link.

On your machine, a pack's settings live in env files at the pack's root: your app's own .env, then .env.local. The secrets you store with kindgi secrets set live in Kindgi's own file, .kindgi/secrets.env, which your app doesn't load. kindgi dev reads them in that order, and a name in more than one takes its value from the last: .kindgi/secrets.env wins.

.env
ORDERS_APP_URL=http://localhost:3000

kindgi dev says which files it read when it starts:

✓ env files: .env, .env.local — 4 name(s) for the pack

Under kindgi dev, the env files are two things at once.

The pack service's environment. Your tools' code runs in the pack service, and the names in your app's env files are in its environment. Ask your coding agent, for example: "I want a tool that gives me the link to an order in our back office." Kindgi's skills in your pack tell it how (How it knows Kindgi). For reference, the tool looks like this. It reads the back office's URL, ORDERS_APP_URL, from its environment:

tools/order-link/index.ts
import { defineTool } from '@kindgi/sdk/define';
import type { ToolId } from '@kindgi/sdk/types';
import { z } from 'zod';
const defined = defineTool({
id: 'my-pack.order-link' as ToolId,
description: 'Returns the link to an order in the back office.',
version: '0.1.0',
input: z.object({ orderId: z.string() }),
output: z.object({ url: z.string() }),
effects: [],
mutating: false,
handler: async ({ orderId }) => {
const base = process.env.ORDERS_APP_URL;
if (base === undefined) throw new Error('ORDERS_APP_URL is not set');
return { url: `${base}/orders/${orderId}` };
},
});
if (defined.kind === 'err') throw new Error(defined.error.message);
export default defined.value;

Call it from my-pack.link. --flow is the flow, and --input its input:

Terminal window
pnpm exec kindgi runs start --flow=my-pack.link --input='{"orderId":"ord_1001"}'
"flowId": "my-pack.link",
"flowVersion": "0.1.0",
"status": "completed",
…
"output": {
"url": "http://localhost:3000/orders/ord_1001"
},
Excerpt

Nothing else reaches it. A variable exported in the shell you start kindgi dev from isn't in the pack service's environment, unless an env file refers to it, as Refer to another value shows. A KINDGI_* name never is, from anywhere. Neither is a secret: no name from .kindgi/secrets.env, and no model provider's key, whichever file holds it. Save an env file and the pack service restarts with the new values.

A value can use another name's, as ${NAME} or $NAME, with ${NAME:-fallback} for when it's unset or empty:

.env
ORDERS_HOST=localhost:3000
ORDERS_APP_URL=http://${ORDERS_HOST}
ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
  • A name the env files define takes the files' value, wherever it's defined.
  • Any other name takes the value from the shell you start kindgi dev from.
  • A key that refers to itself, like ANTHROPIC_API_KEY here, takes the shell's value too. This keeps the key out of the file, as a docker-compose .env does.

If the shell doesn't have it, the value is empty. kindgi dev warns as it starts, and so does kindgi env list:

⚠ 1 warning(s):
.env: ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} takes the environment's ANTHROPIC_API_KEY, and the environment doesn't have it — expanded to ""

With the key exported in the shell, kindgi env list shows it set:

ANTHROPIC_API_KEY sk****************al [.env]

The local secrets store. Tools' declared secrets, HTTP tools' credentials, model providers' keys and MCP endpoints' credentials resolve in an environment. Under kindgi dev, it's local, and local is these files, Kindgi's own last. kindgi secrets set … --env=local writes to .kindgi/secrets.env, as Store a secret shows. The file is owner-only, under .kindgi/, which kindgi init keeps out of git. It's in no file your app loads. A framework like Next.js or Vite loads .env.local into every route of your app, and your app has no use for a model key.

For a value your app reads too, such as a webhook signing secret, add --app. It writes your app's own env file instead, where both sides read it: the last of the files, .env.local by default.

Terminal window
pnpm exec kindgi secrets set ACME_WEBHOOK_SECRET --env=local --scope=tenant --app

It prompts for the value, then prints Set ACME_WEBHOOK_SECRET at tenant in local, in your app's env file.

A model provider's key can sit in .env or .env.local already: put there before Kindgi kept its own file, or because your app calls the provider itself. Kindgi still uses it from there. kindgi dev says so once as it starts, naming the key and the file, never the value.

To give Kindgi its own copy:

Terminal window
pnpm exec kindgi secrets copy

It copies the keys Kindgi's providers use from your app's env files into .kindgi/secrets.env, a line exactly as written, and merges: a name Kindgi's file already holds keeps Kindgi's value. To copy other keys, name them: kindgi secrets copy ANTHROPIC_API_KEY.

It never edits or deletes anything in your app's files. Whether the key stays there is your call: it's your app's file, and your app may use the key itself. A provider's SDK reads ANTHROPIC_API_KEY or OPENAI_API_KEY from the environment without your code naming it. copy says, for each key:

  • that it's still in your app's file, and removing it is up to you;
  • if your app uses the key itself, to keep it there;
  • if the file is tracked by git, that the key is in your repository's history: rotate it with its provider.

When your app calls the provider too, give each its own key. Keep your app's key in its env file, and give Kindgi its own key under the same name in .kindgi/secrets.env, which takes precedence. Each spends on its own key. One shared key in your app's file works as well, but it stays readable by your app and by a coding agent working in it. So keep your agent out of those files, as Your coding agent shows.

List the values in your app's env files. --env=local is the environment kindgi dev serves:

Terminal window
pnpm exec kindgi env list --env=local
Environment: local
Env file: .env
Env file: .env.local
Reveal: NO (redacted; --reveal to show)
4 key(s):
HTTPBIN_TOKEN de**********23 [.env.local]
INVENTORY_TOKEN in**************56 [.env.local]
ORDERS_APP_URL ht*****************00 [.env]
RECEIPT_SIGNING_KEY a6************************************************************83 [.env.local]

Values are redacted. --reveal prints them to a terminal, never to a pipe or a file. kindgi env list shows your app's env files. The names in Kindgi's own file are listed by kindgi secrets list --env=local --scope=tenant, never their values.

To read more of your app's files, list them in the pack's config, lowest precedence first. Kindgi's own .kindgi/secrets.env is always read last. Ask your coding agent, for example: "Have kindgi dev read my other env files too." For reference, it writes the list like this:

// in kindgi.config.ts
dev: { envFiles: ['.env', '.env.development', '.env.local'] },
Excerpt

kindgi dev reads the list when it starts: restart it after a change.

✓ env files: .env, .env.development, .env.local — 6 name(s) for the pack

The files hold secrets. kindgi init adds .kindgi/ and .env.local to your .gitignore. Make sure it covers every env file:

.kindgi/
.env
.env.*
!.env.example

The Python template's .gitignore has these lines. A TypeScript pack made with kindgi init 0.1.0 has no .gitignore, because its lines went into .npmignore. Add one.