Keep local values in env files
This guide keeps your pack's local settings and secrets in env files, and
shows what reads each one under kindgi dev.
Before you start
Section titled “Before you start”- 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.
Where they live
Section titled “Where they live”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.
ORDERS_APP_URL=http://localhost:3000kindgi dev says which files it read when it starts:
✓ env files: .env, .env.local — 4 name(s) for the packWhat reads them
Section titled “What reads them”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:
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;import os
from pydantic import BaseModel, Field
from kindgi import tool
class OrderRef(BaseModel): order_id: str = Field(alias="orderId")
class Link(BaseModel): url: str
@tool(id="my-pack.order-link", mutating=False)def order_link(ref: OrderRef) -> Link: """Returns the link to an order in the back office.""" return Link(url=f"{os.environ['ORDERS_APP_URL']}/orders/{ref.order_id}")package mypack.tools;
import com.kindgi.pack.Tool;
public final class OrderLink { public record Input(String orderId) {}
public record Output(String url) {}
public static final Tool<Input, Output> TOOL = Tool.define("my-pack.order-link") .description("Returns the link to an order in the back office.") .version("0.1.0") .input(Input.class) .output(Output.class) .mutating(false) .handler((input, ctx) -> { String base = System.getenv("ORDERS_APP_URL"); if (base == null) throw new IllegalStateException("ORDERS_APP_URL is not set"); return new Output(base + "/orders/" + input.orderId()); });
private OrderLink() {}}package mypack.tools
import com.kindgi.pack.scaladsl._
object OrderLink { final case class Input(orderId: String) final case class Output(url: String)
val tool: Tool[Input, Output] = Tool[Input, Output]("my-pack.order-link") .description("Returns the link to an order in the back office.") .version("0.1.0") .readOnly .handler { (input, _) => val base = sys.env.getOrElse("ORDERS_APP_URL", throw new IllegalStateException("ORDERS_APP_URL is not set")) Output(s"$base/orders/${input.orderId}") }}Call it from my-pack.link. --flow is the flow, and --input its
input:
pnpm exec kindgi runs start --flow=my-pack.link --input='{"orderId":"ord_1001"}'uv run kindgi runs start --flow=my-pack.link --input='{"orderId":"ord_1001"}'./kindgiw runs start --flow=my-pack.link --input='{"orderId":"ord_1001"}'./kindgiw 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" },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.
Refer to another value
Section titled “Refer to another value”A value can use another name's, as ${NAME} or $NAME, with
${NAME:-fallback} for when it's unset or empty:
ORDERS_HOST=localhost:3000ORDERS_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 devfrom. - A key that refers to itself, like
ANTHROPIC_API_KEYhere, takes the shell's value too. This keeps the key out of the file, as a docker-compose.envdoes.
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.
pnpm exec kindgi secrets set ACME_WEBHOOK_SECRET --env=local --scope=tenant --appuv run kindgi secrets set ACME_WEBHOOK_SECRET --env=local --scope=tenant --app./kindgiw secrets set ACME_WEBHOOK_SECRET --env=local --scope=tenant --app./kindgiw secrets set ACME_WEBHOOK_SECRET --env=local --scope=tenant --appIt prompts for the value, then prints
Set ACME_WEBHOOK_SECRET at tenant in local, in your app's env file.
A key your app's env files already hold
Section titled “A key your app's env files already hold”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:
pnpm exec kindgi secrets copyuv run kindgi secrets copy./kindgiw secrets copy./kindgiw secrets copyIt 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.
See what's set
Section titled “See what's set”List the values in your app's env files. --env=local is the environment
kindgi dev serves:
pnpm exec kindgi env list --env=localuv run kindgi env list --env=local./kindgiw env list --env=local./kindgiw 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.
Read other files
Section titled “Read other files”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.tsdev: { envFiles: ['.env', '.env.development', '.env.local'] },# in pyproject.toml[tool.kindgi.dev]envFiles = [".env", ".env.development", ".env.local"]// in kindgi.config.json"dev": { "envFiles": [".env", ".env.development", ".env.local"] }// in kindgi.config.json"dev": { "envFiles": [".env", ".env.development", ".env.local"] }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 packKeep them out of git
Section titled “Keep them out of git”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.exampleThe 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.