Secrets, MCP servers and environments belong to a team. Agents use them by name, and Kiln resolves them for each run.
Secrets
A secret has a scope, a name, a type, an optional description, and who created and last changed it and when. Values are write-only: the API returns metadata, never values. A value can be at most 128 KiB.
| Scope | Who manages it | Which runs get it |
|---|---|---|
team |
Team owners and admins | Runs of the team's factories, when the agent asks for it |
personal |
You | Only runs you triggered: work you sent, or work whose requester resolves to your account |
When both scopes have a secret with the same name, a run you triggered gets your personal one.
Types
| Type | Value | Gives the run |
|---|---|---|
raw |
a string | An environment variable named after the secret |
docker_registry |
{ host, username, password } |
Nothing in the environment. It is a private image credential. See Private images. |
aws_ecr |
{ roleArn, region } (team scope only) |
Nothing in the environment. It is an image credential. See Private images. |
anthropic_api_key |
a string | ANTHROPIC_API_KEY |
claude_oauth_token |
a string from claude setup-token |
CLAUDE_CODE_OAUTH_TOKEN |
chatgpt_auth |
Codex's auth.json from a ChatGPT sign-in (use Sign in with ChatGPT under Team settings → Model keys) |
Nothing in the environment. Codex runs get it as their auth.json. See ChatGPT logins. |
anthropic_bedrock_api_key |
a string, or { apiKey, region } |
AWS_BEARER_TOKEN_BEDROCK, AWS_REGION, CLAUDE_CODE_USE_BEDROCK=1 |
anthropic_bedrock_access_key |
{ accessKeyId, secretAccessKey, sessionToken?, region } |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_REGION, CLAUDE_CODE_USE_BEDROCK=1 |
openai_api_key |
a string | OPENAI_API_KEY |
openrouter_api_key |
a string | OPENROUTER_API_KEY |
google_api_key |
a string | GOOGLE_GENERATIVE_AI_API_KEY |
Kiln checks each value against its type. Secret names use letters, digits and underscores.
Manage secrets with GET /api/v1/managed-secrets, POST /api/v1/managed-secrets, PATCH /api/v1/managed-secrets/<scope>/<name> (new value or description) and DELETE /api/v1/managed-secrets/<scope>/<name>.
How runs get secrets
- Agent secrets: an agent's
secretslist names the secrets its runs get. Typed secrets give their provider variables. Agents get no secrets they do not list. - Harness credentials: with
harness.auth: { source: secret, secretName: X }, a typed secretXgives its provider variables. A raw secret goes to the harness's usual variable. Withoutharness.auth, Kiln picks the first credential the harness can use, from typed secrets and the names in Harnesses. Claude Code also accepts Bedrock credentials. - Lookup order for a name: your personal secret (for runs you triggered), then the team secret, then the server's secrets (the older admin
PUT /api/v1/secrets/<name>store, model-provider environment variables andKILN_SECRET_<NAME>). - Redaction: Kiln replaces every secret value a run could see with
[redacted]in its events, tool output and summary. - Encryption: with
KILN_SECRET_KEYset, values are sealed with AES-256-GCM in the store. Without it they are stored in plain text, so set it on any shared server. - Rotating the key: set the new value as
KILN_SECRET_KEYand the old one asKILN_SECRET_KEY_PREVIOUS, then restart the API. It reads secrets sealed with either key, reseals them (and the GitHub App's credentials) with the new one, and logs how many. RemoveKILN_SECRET_KEY_PREVIOUSafterwards. Run tokens signed with the old key stop working at once, so runs in progress lose access to the API; a new key alone, without the previous one, leaves sealed secrets unreadable and the API does not start.
Team-managed model keys
Team owners and admins set one key per provider with PUT /api/v1/teams/<team>/model-keys/<provider>:
| Provider | Team secret | Type |
|---|---|---|
anthropic |
KILN_MODEL_ANTHROPIC |
anthropic_api_key |
openai |
KILN_MODEL_OPENAI |
openai_api_key |
openrouter |
KILN_MODEL_OPENROUTER |
openrouter_api_key |
bedrock |
KILN_MODEL_BEDROCK |
anthropic_bedrock_access_key, or anthropic_bedrock_api_key when you send an API key |
Runs without their own harness credential use these. GET /api/v1/teams/<team>/model-keys shows which keys are set.
MCP servers ("MCPs and apps")
Team owners and admins register MCP servers for the team. Agents name them in mcpServers, by name or id.
A server is opt-in: an agent gets it only when it lists it. Turn on Give to all agents (allAgents: true in the API) and every agent in every factory of the team gets it on top of its own list, including automation runs that replace mcpServers. Use it for shared tools like an observability server.
{ "name": "figma", "url": "https://mcp.figma.com/mcp", "headers": { "Authorization": "Bearer {{FIGMA_TOKEN}}" } }
{ "name": "sentry", "command": "npx", "args": ["-y", "@sentry/mcp-server"], "env": { "SENTRY_TOKEN": "{{SENTRY_TOKEN}}" }, "mode": "direct" }- A server has either
commandwithargsandenv, orurlwithheaders. {{NAME}}refers to a secret, looked up the same way as agent secrets.- Connection mode
managed(the default for URL servers): Kiln puts the secret values into the harness's MCP config. - Connection mode
direct(the default for command servers): the config keeps${NAME}references, and the values go into the run's environment. Values are never written to disk. - A server whose secrets are missing, or a name Kiln does not know, is skipped. The run shows an
mcp_skippedevent. - A run may reach the servers it gets: the host of a URL server, and the host of any URL-valued secret a server uses (such as
JIRA_BASE_URL), are added to its egress allowlist.
Templates
New under MCPs and apps, or Add MCP on an agent, offers templates for common servers: observability (SigNoz, Datadog, New Relic, Better Stack), Sentry, Figma, Linear, GitHub, Playwright and Postgres. A template asks for what the server needs, one labeled field each, with where to find the value. Keys are saved as team secrets and the server is registered for the team under the template's name, so any agent can list it. Some templates ask first how the server runs: SigNoz offers SigNoz Cloud (the hosted MCP endpoint for your region) or Self-hosted (your own SigNoz; each run starts signoz-mcp-server, which the sandbox image ships, with your SigNoz URL and API key). A run can reach the URLs a server is configured with, so a self-hosted SigNoz needs no egress change. Anything else is a custom server: paste its JSON.
Testing servers
Test connects to a server with the team's secrets the way a harness would (MCP initialize, then tools/list) and shows its tools, or what went wrong: refused credentials, a wrong path, an unreachable host or a missing secret. The same test runs in every scope:
| Where | What it tests |
|---|---|
| MCPs and apps → Test all, or Test on a row | The team's servers, or one of them |
| Factory settings → agent and scorer defaults | The servers in the list, with the factory team's secrets |
| An agent's settings | That agent's servers |
API: POST /api/v1/mcp-servers/test with {} (the team, admins only), { "factory": "<id>" }, { "factory": "<id>", "agent": "<name>" }, and optionally "servers": ["<name>"] to narrow any of them. Command servers start only inside a run's sandbox, so their test checks their secrets and reports untested. Kiln never tests servers on localhost, loopback or link-local addresses (such as the cloud metadata service).
Built-in integration servers
These servers can be referenced by id without registering them. They are listed as connected once their secrets exist in the team or on the server.
kiln_id |
Server | Needs |
|---|---|---|
linear |
Linear's hosted MCP | LINEAR_API_KEY |
slack |
@modelcontextprotocol/server-slack |
SLACK_BOT_TOKEN |
jira |
mcp-atlassian |
JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN |
github |
GitHub's hosted MCP | GITHUB_MCP_TOKEN (use a read-mostly token; agents never get merge rights) |
discord |
mcp-discord |
DISCORD_BOT_TOKEN |
Every run also gets the Kiln MCP server with its own run token.
Environments
An environment is a team template: a Docker image, repositories, setup commands and secrets. Point a factory at one with environmentId in agentDefaults, an agent or an automation. You can use the id or the name. The most specific setting wins.
# factory.yaml
agentDefaults:
harness: { type: claude-code }
environmentId: node-24At run time:
- The image is used when the agent names no runner.
- The environment's repositories are cloned next to the factory's.
- Its setup commands run after the runner's setup commands.
- Its secrets are added to the agent's secrets.
- An
environmentIdthat the team does not have stops the run with a clear error.
Manage environments with GET, POST, PATCH and DELETE /api/v1/environments.
Project environments
A project environment is a named set of variables and secrets, such as preview or production, that a project's factories and Studio Apps get. Use it for settings an app needs to run, like the API URL a preview calls.
Variables are set in layers. Each later layer replaces a key from an earlier one:
| Layer | Who changes it | Applies to |
|---|---|---|
| Team | Team owners and admins | Every factory in a project and every Studio App in the team |
| Project | Team owners and admins, and the project's owner | The project's factories and Apps |
| Factory | Admins of the factory | That factory |
| App | Team owners and admins, and the App's creator | That Studio App |
A factory that is in no project gets no project environment variables, not even the team's.
Where variables go
Both use the preview environment:
- Factory runs: the variables are added to the run's environment. Kiln's own variables (
KILN_*), harness credentials and MCP server secrets win over a variable with the same name. - PR previews: the preview provider gets the factory's variables. The Kubernetes provider puts them in a Secret named
app-envin the preview's namespace and loads it withenvFrom. Vercel deployments use the variables set on the Vercel project.
Secrets
Turn on Secret for a variable to keep it write-only. Secret values are sealed with KILN_SECRET_KEY like team secrets, never returned by the API or shown in the UI, and replaced with [redacted] in run output. To change other variables, leave a saved secret's value empty: Kiln keeps it.
Rules
- Environment names use lowercase letters, digits and dashes, starting with a letter, at most 32 characters.
- Keys use letters, digits and underscores, and can't start with
KILN_. - A layer holds at most 200 variables. A value can be at most 128 KiB.
- Every change is in the audit log as
variables.updatedorvariables.deleted, with the keys and never the values.
Managing them
Open a project and use Environments: one card per environment, with the team's variables it inherits listed underneath.
Through the API:
| Route | Does |
|---|---|
GET /api/v1/variables?scope=<scope>&owner=<id> |
Lists an owner's layers. scope is team, project, factory or app; for team, owner is the team id. |
PUT /api/v1/variables/<scope>/<owner>/<environment> |
Replaces a layer: { "variables": [{ "key": "API_URL", "value": "https://api.example.com" }, { "key": "TOKEN", "secret": true }] } |
DELETE /api/v1/variables/<scope>/<owner>/<environment> |
Deletes a layer |
GET /api/v1/variables/resolve?environment=preview&factory=<id> |
Shows what a factory, App (app=) or project (project=) gets, with the layer each key comes from |
Private images
A runner pulls a private image with a team secret of type docker_registry or aws_ecr:
# runners/private.yaml
image: ghcr.io/acme/runner:2
backend: docker
platform:
linux:
registryCredentialSecretName: GHCRAn environment does the same with registryCredentialSecretName next to its image. That credential is used when the agent names no runner.
docker_registry: the secret'shostmust be the image's registry. Bare names such asacme/runnerare on Docker Hub (docker.io).aws_ecr: the image must be on an ECR registry (<account>.dkr.ecr.<region>.amazonaws.com). Kiln gets a short-lived password by runningaws ecr get-login-password --region <region>on the server, withAWS_ROLE_ARNset to the secret's role. The server's AWS credentials must be allowed to callecr:GetAuthorizationToken.- Docker pulls with a temporary
DOCKER_CONFIGthat is deleted right after the pull. The credential never enters the container. - Kubernetes gets a
kubernetes.io/dockerconfigjsonSecret as the pod'simagePullSecrets. Kiln deletes it with the sandbox.