Configuration¶
Docs index ยท Getting started ยท Troubleshooting
The server, the worker and the pnpm graft commands read their settings from the environment. src/bin/* loads .env if it exists (copy .env.example); tests never read .env. Validation lives in src/config/index.ts: an unset or empty variable takes its default, then the value is checked, and an error names the variable and the rule only, never the value. The evaluation tool has its own copy of the relevant rules in src/eval/env.ts, so it never needs a database URL or the webhook secret.
"Default" is the value used when the variable is unset; ".env.example" shows what the shipped example sets when that differs.
Server and worker¶
| Variable | Default | Rule and use |
|---|---|---|
NODE_ENV |
development |
development, test or production. In production the .env.example webhook secret is refused. |
HOST |
0.0.0.0 (.env.example: 127.0.0.1) |
Listen address. Keep the loopback for local runs (smee forwards to localhost); use 0.0.0.0 only inside a container. |
PORT |
3000 |
1 to 65535. Also used by pnpm tunnel. |
LOG_LEVEL |
info |
trace, debug, info, warn, error, fatal or silent. |
DATABASE_URL |
required (.env.example: postgres://graft:graft@localhost:55432/graft) |
A postgres:// or postgresql:// URL. The compose database is bound to 127.0.0.1:55432. |
GITHUB_APP_ID |
required (.env.example: 123456) |
The App's numeric ID. Any number works for a local run without a real App. |
GITHUB_APP_PRIVATE_KEY_PATH |
(.env.example: ./dev.pem) |
Path to the App's PEM key. Set exactly one of this and GITHUB_APP_PRIVATE_KEY. Keep the file outside the repository for a real App. |
GITHUB_APP_PRIVATE_KEY |
unset | The PEM inline; a literal \n is accepted. Exactly one of the two key variables. |
GITHUB_WEBHOOK_SECRET |
required (.env.example: a placeholder) | At least 16 characters; must equal the App's webhook secret. Generate with openssl rand -hex 32. The placeholder is refused when NODE_ENV=production. |
GITHUB_API_URL |
https://api.github.com |
A URL. |
PGBOSS_SCHEMA |
pgboss |
Postgres schema for pg-boss; ^[a-z_][a-z0-9_]*$. |
WORKER_CONCURRENCY |
5 |
1 to 50 jobs at a time per worker. |
WEBHOOK_BODY_LIMIT_BYTES |
26214400 |
Webhook body cap in bytes (GitHub's limit is 25 MB). |
SMEE_URL |
unset | A URL from https://smee.io/new; only used by pnpm tunnel. |
GRAFT_AGENT_LOGINS |
empty | Comma-separated GitHub logins that always count as agent accounts (letters, digits, -, optional [bot]; lowercased). Also excluded from gate votes. |
GRAFT_AGENT_COAUTHOR_EMAILS |
noreply@anthropic.com |
Comma-separated Co-authored-by emails that mark a commit as agent-written (default: Claude Code's). |
GITHUB_APP_SLUG |
unset | The App's slug. Enables the <slug>[bot] fallback for recognising Graft's own PR comment when performed_via_github_app is missing (decision D13). |
ANTHROPIC_API_KEY |
unset | Sent as x-api-key. Without a credential (this, ANTHROPIC_AUTH_TOKEN or federation) the service still runs and every card shows as unavailable with the rule-based risk floor. Never commit a real key. |
ANTHROPIC_AUTH_TOKEN |
unset | Sent as Authorization: Bearer, for gateways that expect a bearer token (OpenRouter, Vercel AI Gateway, Bedrock Mantle). Set at most one way to authenticate: the key, this token or federation. |
ANTHROPIC_FEDERATION_RULE_ID |
unset | Workload Identity Federation: the federation rule (fdrl_...). Federation needs this, ANTHROPIC_ORGANIZATION_ID, ANTHROPIC_SERVICE_ACCOUNT_ID and ANTHROPIC_IDENTITY_TOKEN_FILE, all or none, and no key or token. The only federation value that is logged. See Deploying with federation. |
ANTHROPIC_ORGANIZATION_ID |
unset | Federation: the organization UUID (Claude Console, Settings, Organization). |
ANTHROPIC_SERVICE_ACCOUNT_ID |
unset | Federation: the service account (svac_...) the minted token acts as. |
ANTHROPIC_WORKSPACE_ID |
unset | Federation, optional: the workspace (wrkspc_...) to scope the token to. Required when the rule is enabled for more than one workspace. |
ANTHROPIC_IDENTITY_TOKEN_FILE |
unset | Federation: absolute path to the OIDC JWT your platform writes (for example a projected Kubernetes token). Read on every exchange, never at startup, and never logged. |
ANTHROPIC_BASE_URL |
unset (the Claude API) | An http(s) URL of any service that speaks the Anthropic Messages API, for example https://openrouter.ai/api. Diffs go to that service: check its data retention first. Only the host is logged. With federation the token exchange goes to the same host, so it must be https (or loopback). |
GRAFT_LLM_MODEL_FAST |
claude-haiku-5-5 |
First-pass writer when the rule-based floor is trivial. A Claude model ID, optionally with a gateway's vendor prefix or version suffix (anthropic/claude-haiku-5.5, us.anthropic.claude-haiku-5-5-v1:0). |
GRAFT_LLM_MODEL_STRONG |
claude-sonnet-5-5 |
Writer for everything else, and always the verifier. Same pattern. |
GRAFT_LLM_TIMEOUT_MS |
120000 |
Per-call timeout, 5,000 to 600,000 ms. |
GRAFT_LLM_MAX_RETRIES |
2 |
SDK retries, 0 to 5. |
GRAFT_LLM_REFUSAL_FALLBACK |
true |
true or false: let the API retry a refused request on a substitute model (Sonnet and Opus only). Applies only on the Claude API: with an ANTHROPIC_BASE_URL other than api.anthropic.com it is never sent. |
GRAFT_LLM_REFUSAL_FALLBACK_MODEL |
unset (off) | A model Graft itself retries once when a call is refused and no server-side fallback ran: on a gateway or cloud, and for Haiku on the Claude API. The card names the model that answered. Same pattern as the model settings. |
GRAFT_LLM_DAILY_TOKEN_BUDGET |
2000000 |
Per-repository daily token budget (UTC day, from the intent_spend ledger); 0 means unlimited. |
GRAFT_INTENT_SENSITIVE_PATHS |
empty | Extra comma-separated globs that set the risk floor to sensitive, for example services/billing/**. 1 to 200 characters each, no leading !, no .. segment, and the same checks as graft.yaml globs (validated at startup). |
GRAFT_FEEDBACK_POLL_CRON |
*/15 * * * * |
Cron for polling ๐/๐ reactions on the managed comment, or off. Checked for shape only (5 fields). |
GRAFT_INCIDENT_LABEL |
incident |
Label that marks a revert or fix PR as an incident, or off. 1 to 50 characters of [A-Za-z0-9 ._:/-]. |
GRAFT_METRICS_ROLLUP_CRON |
15 2 * * * |
Cron for the daily metrics rollup (recomputes the last 7 UTC days), or off. |
GRAFT_METRICS_EXCLUDED_AUTHORS |
dependabot[bot],renovate[bot] |
Logins left out of the baseline, the rollup PR counts and the gate coverage. |
Deploying with federation¶
Workload Identity Federation lets Graft authenticate to the Claude API without a long-lived key: the platform it runs on issues an OIDC JWT, Graft exchanges it at POST /v1/oauth/token for a token that lasts as long as the federation rule says (one hour by default), and the SDK exchanges again shortly before it expires or after a 401. Set it up in the Claude Console (an issuer, a service account and a federation rule; see Anthropic's Workload Identity Federation guide), then set the four required variables above.
- The host must issue OIDC tokens. Kubernetes (projected service account tokens, including GKE and EKS), Google Cloud Run, Azure (managed identity or workload identity), GitHub Actions and Fly.io (custom OIDC tokens) do. The token has to reach Graft as a file: a projected volume on Kubernetes, or a small sidecar or startup script that fetches it from the metadata service and rewrites the file on other platforms. Render and similar hosts that issue no OIDC token cannot use federation: keep
ANTHROPIC_API_KEYthere. - Tokens are single use. An assertion with a
jticlaim can be exchanged only once, so the file must hold a fresh token before every exchange. Kubernetes rotates projected tokens on its own (setexpirationSecondswell below the rule's maximum lifetime); a script that writes the file must refresh it more often than the minted token expires. A replayed token fails with a 401 and shows asjti_reusedin the Console's authentication history; Graft reports the call asauth. - Remove the old key. Graft refuses to start when
ANTHROPIC_API_KEYorANTHROPIC_AUTH_TOKENis set alongside federation, rather than letting the key silently win. Unset the variable (do not leave it empty in a secret store that other tools also read: the SDK and Claude Code treat an empty key as set). - Failures. A rejected token, rule or ID and an unreadable token file map to
auth; an unreachable token endpoint or a 5xx tounavailable. The card shows "unavailable" either way; the log line carries the HTTP status, never the token or the response body.
Tool and shell-only variables¶
These are read by the CLIs, tests or hooks, not by the server and worker. Secrets among them belong in the shell or a password manager, never in .env.
| Variable | Read by | Default and use |
|---|---|---|
GRAFT_EVAL_DIR |
pnpm eval |
Directory holding pilot case sets; default ~/.graft-eval. Must be outside every git working tree; exclude it from backups and cloud sync. |
GRAFT_EVAL_KEY |
pnpm eval |
Base64 of 32 random bytes that encrypts pilot sets (pnpm eval keygen makes one). Shell only: the CLI ignores a value that only .env provides and warns. |
GRAFT_EVAL_GITHUB_TOKEN |
pnpm eval export --auth token |
Read-only fine-grained token, for pilots without the App installed. |
GRAFT_EVAL_DEBUG |
pnpm eval |
1 adds the message to unmapped errors (which otherwise print their class name only). |
GRAFT_BASELINE_GITHUB_TOKEN |
pnpm graft baseline --auth token |
Token for a dry-run baseline of a repository the App is not installed on (GET requests only). |
GRAFT_E2E_ACTOR_TOKEN |
pnpm graft e2e |
The actor (repository owner) token; falls back to gh auth token. Shell only: the runner refuses to start if .env holds any GRAFT_E2E_* key or the actor token's value. |
GRAFT_E2E_OUTSIDER_TOKEN |
pnpm graft e2e |
Optional second account that votes without being a reviewer. Shell only. |
GRAFT_AGENT, GRAFT_MODEL, GRAFT_TASK, GRAFT_SESSION |
scripts/pilot/prepare-commit-msg |
Values for the Graft-* trailers; the hook does nothing without GRAFT_AGENT. |
TEST_DATABASE_URL |
integration tests | Default postgres://graft:graft@localhost:55432/graft_test; the database is dropped and recreated on every run. |
GRAFT_PERF |
unit tests | 1 (or pnpm test:perf) enforces the millisecond bounds of the performance assertions. |
The e2e runner also sets GRAFT_E2E_GIT_TOKEN (for its own git child, through GIT_ASKPASS) and GRAFT_E2E_OFFLINE_OUT, GRAFT_E2E_OFFLINE_LLM and GRAFT_E2E_OFFLINE_SCENARIOS (for the --only-offline test child). They are internal; do not set them yourself.
Files¶
| File | Purpose |
|---|---|
.env.example |
The local defaults; copy to .env. |
docker-compose.yml |
Postgres 17 on 127.0.0.1:55432 (user, password and database graft). |
github/app-manifest.json |
The App's permissions and events (pinned by test/unit/manifest.test.ts). |
graft.yaml (in each target repository) |
The policy; see features/policy.md. |
e2e/sandboxes.json |
The sandbox repositories the live e2e runner may write to. |