Environment Variables
This page lists the environment variables Paperclip reads for server configuration and the variables it injects into agent processes at runtime.
Use it when you are wiring a deployment, debugging a startup issue, or checking what an adapter can see inside its process environment.
Server Configuration
| Variable | Default | Meaning |
|---|---|---|
PORT |
3100 |
Server port |
HOST |
127.0.0.1 |
Server host binding |
DATABASE_URL |
embedded PostgreSQL | PostgreSQL connection string |
DATABASE_MIGRATION_URL |
falls back to DATABASE_URL |
Optional PostgreSQL URL used only when running migrations — useful when your runtime user lacks DDL rights and a separate role applies schema changes. |
PAPERCLIP_HOME |
~/.paperclip |
Base directory for all Paperclip data |
PAPERCLIP_INSTANCE_ID |
default |
Instance identifier for multiple local instances |
PAPERCLIP_DEPLOYMENT_MODE |
local_trusted |
Runtime mode override |
SERVE_UI |
true (from server.serveUi in config.json) |
When set, overrides the file-config flag that controls whether the server serves the bundled UI. SERVE_UI=true enables it; SERVE_UI=false disables it. |
PAPERCLIP_BIND |
inferred from HOST |
Bind mode for the server socket. One of the values in BIND_MODES (see packages/shared); overrides server.bind in config.json. |
PAPERCLIP_BIND_HOST |
inferred | Custom host when PAPERCLIP_BIND is set to a custom mode; overrides server.customBindHost. |
PAPERCLIP_TAILNET_BIND_HOST |
auto-detected via tailscale ip -4 |
Tailnet IPv4 address the server binds to when bind mode is tailnet. Set explicitly to skip the tailscale CLI probe. |
PAPERCLIP_WORKSPACE_GIT_SCAN_CONCURRENCY |
2 |
How many expensive full-tree workspace Git scans may run at once. Clamped to 1–16. |
PAPERCLIP_WORKSPACE_GIT_SCAN_QUEUE_CAPACITY |
32 |
How many scans may wait in the queue before new ones are rejected. Clamped to 0–1024. |
PAPERCLIP_WORKSPACE_GIT_SCAN_TIMEOUT_MS |
8000 |
Per-scan timeout, in milliseconds, before a quick workspace Git scan (the changed-files browser and cleanliness checks) is abandoned. Clamped to 100–120000. Workspace snapshots use PAPERCLIP_WORKSPACE_GIT_SNAPSHOT_TIMEOUT_MS instead. |
PAPERCLIP_WORKSPACE_GIT_SCAN_CACHE_TTL_MS |
10000 |
How long a completed scan's result is reused before a fresh scan runs, in milliseconds. Clamped to 0–60000. |
PAPERCLIP_WORKSPACE_GIT_SNAPSHOT_TIMEOUT_MS |
1800000 (30 minutes) |
How long a workspace snapshot may take, in milliseconds, before it's abandoned. Snapshots list a workspace's changed, untracked, and ignored files before a sandbox run, and the deadline includes time spent waiting on a slow disk. Raise it for very large trees or slow storage. Values below 1000 fall back to the default; values above 86400000 (24 hours) are capped there. |
PAPERCLIP_WORKSPACE_MANIFEST_MIN_FREE_BYTES |
268435456 (256 MiB) |
Free disk space, in bytes, that workspace snapshots always leave on the host. Snapshots write their file lists to temporary on-disk manifests and stop with an error rather than eat into this reserve. Values below 67108864 (64 MiB) fall back to the default. |
PAPERCLIP_RUNNER_API_COMPANY_CAPTURE_MAX_BYTES |
21474836480 (20 GiB) |
Total bytes of large API responses that Paperclip Runner agents can save for one company. When an agent's API call returns more than fits in a single reply, the response is saved as a company asset the agent pages through, and this cap bounds how much those saved responses can add up to. Deleting the assets frees the space. Values below 1073741824 (1 GiB) fall back to the default. |
PAPERCLIP_RUNNER_API_TOOLS_ENABLED |
on (unset) | Whether Paperclip Runner agents get the broad Paperclip API tools (search_api, call_api, and hire_agent). They're on by default; set the variable to anything other than true to switch them off for every company. |
PAPERCLIP_RUNNER_API_TOOLS_COMPANY_IDS |
unset (all companies) | Comma-separated company IDs. When set, only those companies' Runner agents get the API tools. It can only narrow access — it has no effect if PAPERCLIP_RUNNER_API_TOOLS_ENABLED turns the tools off. |
PAPERCLIP_WORKSPACE_REAPER_COOLDOWN_DAYS |
7 |
How many days the terminal-workspace reaper waits after an issue tree becomes terminal before archiving its workspace. Someone can reopen the work inside this window. 0 disables the cooldown and restores immediate reaping; a negative or non-numeric value falls back to the default. |
Note:
DATABASE_URLis the main switch between the embedded database and external PostgreSQL.
Deployment And Auth
These variables matter most once you move beyond a default local install.
| Variable | Meaning |
|---|---|
PAPERCLIP_PUBLIC_URL |
Canonical public URL for invites, redirects, and auth origin wiring. |
PAPERCLIP_AUTH_PUBLIC_BASE_URL |
Explicit auth base URL when you want Better Auth to use a fixed public origin. |
BETTER_AUTH_URL |
Alternate Better Auth base URL input. |
BETTER_AUTH_SECRET |
Signing secret for Better Auth sessions and tokens. Falls back to PAPERCLIP_AGENT_JWT_SECRET when unset; the server refuses to start if neither is configured. For local development the .env.example ships paperclip-dev-secret. |
BETTER_AUTH_BASE_URL |
Alternate Better Auth base URL input used by some deployments. |
BETTER_AUTH_TRUSTED_ORIGINS |
Comma-separated allowlist of trusted auth origins. |
PAPERCLIP_AGENT_JWT_SECRET |
Secret used to mint agent API JWTs. Required for local adapter auth. |
PAPERCLIP_AGENT_JWT_TTL_SECONDS |
Agent JWT lifetime in seconds. |
PAPERCLIP_AGENT_JWT_ISSUER |
Agent JWT issuer. |
PAPERCLIP_AGENT_JWT_AUDIENCE |
Agent JWT audience. |
PAPERCLIP_TOOL_ACTION_SIGNING_SECRET |
Signing secret for tool action approvals. Unlike BETTER_AUTH_SECRET it has no fallback — if it is unset, the server cannot issue signed tool action approvals and raises ToolActionSigningSecretMissingError. Set it in the instance's own environment; worktrees inherit it from .paperclip/.env. For local development the .env.example ships paperclip-dev-tool-action-signing-secret-change-me. |
Related deployment variables:
| Variable | Meaning |
|---|---|
PAPERCLIP_DEPLOYMENT_EXPOSURE |
Exposure policy override, typically private or public in authenticated mode. |
PAPERCLIP_AUTH_BASE_URL_MODE |
Base URL handling mode, such as auto or explicit. |
PAPERCLIP_ALLOWED_HOSTNAMES |
Comma-separated allowlist for authenticated/private host validation. |
TRUST_PROXY |
How much to trust the X-Forwarded-For header when the server sits behind a reverse proxy or load balancer. Defaults to unset (trust nothing). See below. |
Tip: If
paperclipai doctoris failing on hostnames, redirects, or auth origins, inspect this group first.
Trusting a reverse proxy (TRUST_PROXY)
When you run Paperclip behind a load balancer or reverse proxy (nginx, Caddy, a cloud LB), the real client IP arrives in the X-Forwarded-For header rather than on the socket. TRUST_PROXY tells the server how far to trust that header so req.ip and rate-limiting see the actual client instead of your proxy.
The default is unset, which trusts nothing — the safe choice, because an untrusted client can otherwise spoof its address by sending its own X-Forwarded-For. Only opt in when there really is a proxy in front of the server.
Accepted values:
| Value | Meaning |
|---|---|
unset, "", false, 0 |
Trust nothing. The default. |
true |
Trust the header unconditionally. Unsafe unless the server is unreachable except through your proxy. |
a positive integer (e.g. 1) |
Trust that many proxy hops. Use 1 for a single LB in front of the server. |
| a comma-separated list | Trust specific sources by named subnet (loopback, linklocal, uniquelocal) or CIDR (e.g. 10.0.0.0/8, fd00::/8). |
A malformed value (a stray sign, leading zeros, or an unrecognised token) makes the server refuse to start with an explanatory error, so a typo fails loudly rather than silently disabling proxy trust. After setting it, confirm req.ip in the request log matches the real client IP through your proxy.
Cloud-Managed Instances
| Variable | Default | Meaning |
|---|---|---|
PAPERCLIP_MANAGED_CONFIG |
unset (self-hosted) | Set only by the Paperclip Cloud harness. Holds one JSON document describing which experimental feature flags the harness manages for this instance, the plugin keys it declares for auto-install, and optionally the sandbox environment it provisions for you at boot. Absent means self-hosted: no overlay, no behavior change. Present but blank, or malformed, makes the server refuse to start. |
You don't set this variable yourself. It exists so that a hosted instance gets its feature configuration from the fleet that runs it rather than from its own database, and it's documented here so you can recognise it in a running container's environment.
The value is a single JSON document:
{
"v": 1,
"mode": "cloud",
"catalogVersion": "2026.720.0",
"features": { "enableApps": false, "enableCases": true },
"plugins": { "autoInstall": ["daytona", "kubernetes"] },
"environments": [
{ "name": "Daytona", "provider": "daytona", "config": { "target": "us" } }
]
}
Six top-level keys are allowed — v, mode, catalogVersion, features, plugins, and environments — and anything else makes the server refuse to start.
Five of them are required: v, mode, catalogVersion, features, and plugins.autoInstall, though features may be {} and plugins.autoInstall may be []. mode is always "cloud", and catalogVersion records the feature-catalog version the document was validated against.
environments is the one optional section. It arrived later than the others, and documents delivered before it existed still have to boot on newer builds — a fleet image roll can't be lockstepped with a config re-delivery — so leaving it out simply means "this instance declares no managed environment". That's different from a missing features or plugins.autoInstall, where absence would silently drop a control the harness meant to be in force. When the section is present, it's validated just as strictly as everything else. The sandbox environment it provisions below has the details.
It fails closed
Only an absent variable means self-hosted. Everything else is treated as a harness misconfiguration and stops the server at startup with a precise error, so a truncated or mistyped document can never silently drop a control that was supposed to be in force. The server refuses to start when the variable is:
- set but blank (unset it entirely for self-hosted mode)
- not valid JSON, or not a JSON object
- carrying an unknown top-level key, or a
vother than1, or amodeother than"cloud" - missing
featuresorplugins.autoInstall - naming a feature key this build doesn't have, or one whose feature-catalog tier isn't
managed - giving a non-boolean value for a feature, or a blank/duplicate plugin key
- carrying a malformed
environmentssection — more than one entry, an unknown key inside an entry, a blanknameorprovider, aproviderthat isn't also listed inplugins.autoInstall, aconfigthat setsprovideritself, or aconfigkey that looks like a credential
What it can and can't reach
features may only target the boolean feature-flag keys of the instance's experimental settings — the ones on Settings → Experimental. Server-managed bookkeeping fields (activation cutoffs, lookback hours) aren't booleans and are excluded by construction, so a managed document can't touch them.
On top of that, each flag carries a tier in the build's feature catalog: preference (a tenant's own taste setting the harness leaves alone), managed (the harness may set it), or floor (pinned in code). Only managed keys are accepted. A key whose tier differs in the running build is version skew between the document's catalogVersion and the app, and the server refuses to start rather than apply a control with mismatched semantics.
Paperclip publishes that tier map per release as a feature-catalog.json artifact — generated by scripts/generate-feature-catalog.ts --version <catalogVersion> — which the cloud harness imports and validates its feature writes against.
Nothing is written to the database
The overlay is never persisted. The values are applied when settings are read, which means they re-assert on every read — a database restore or a hand-edited settings row cannot resurrect a capability the harness disabled.
In the UI, every key the overlay controls renders with a lock badge reading Managed by Paperclip Cloud and a disabled toggle. Self-hosted responses carry no managed keys at all, so every toggle stays editable. See Experimental features for the user-facing view.
The sandbox environment it provisions
If you open Settings → Instance settings → Environments on a hosted instance and find a sandbox environment already waiting there — one you never created — the environments section is where it came from. The fleet declares it, and the server sets it up for you on boot so agents have somewhere to run without you wiring up a provider first.
The feature overlay above is read-time only, but this part is different: an entry provisions a real environment row you can see in the UI, and the server re-applies it on every boot. That's safe to repeat — the step is idempotent, so a restart refreshes the row rather than piling up duplicates.
Each entry looks like this:
{
"name": "Daytona",
"description": "Managed sandboxes for agent runs",
"provider": "daytona",
"config": { "target": "us" }
}
nameis the display name of the environment row.descriptionis optional. It mirrors the document, so dropping it later clears the description rather than pinning the old one forever.provideris the sandbox plugin's driver key — the same key the plugin uses inplugins.autoInstall, likedaytonaorkubernetes. It has to appear in that auto-install list too: on a managed instance the fleet is the only way a plugin gets installed, so an environment whose provider plugin isn't provisioned could never serve a run. That mismatch is caught at startup rather than at the first run.configis stored verbatim on the environment row and validated by the provider plugin when it acquires a lease. See Sandbox Providers for the fields each provider accepts.
One entry, and never any credentials
Two rules shape what this section is allowed to declare.
At most one entry. The database enforces a single Paperclip-managed sandbox row per instance (a partial unique index named environments_managed_sandbox_idx), and every entry in the list provisions that one row — so a longer list could never be satisfied and is rejected at parse time. For the same reason, a non-empty environments section and a forced PAPERCLIP_EXECUTION_MODE are mutually exclusive: both claim the same row, and configuring both stops the server rather than letting boot order pick a winner. An empty "environments": [] declares nothing, so it sits alongside a forced execution mode without complaint.
No secrets, ever. A managed-config document carries no credentials, so any config key that looks like one — matching api_key, apiKey, token, secret, password, or credential, at any nesting depth — is treated as a misrouted credential and refuses startup. Provider credentials reach a managed instance as ordinary process environment variables instead: every bundled sandbox provider falls back to its own documented variable, such as DAYTONA_API_KEY, when config leaves the key out.
When the provider isn't ready
Provisioning waits for the bundled-plugin startup pass to finish, then checks that the entry's provider plugin is installed, ready, and running a live worker. A plugin whose record says ready but whose activation failed has no worker and can't serve a lease, so that check matters.
If the provider isn't up, Paperclip skips the entry rather than writing an active row — and if an earlier boot already provisioned one, it archives that row so run scheduling stops selecting an environment whose leases would fail. The server logs the reason and keeps booting; the cost is that this one environment is unavailable, not a fleet stuck in a crash loop.
Recovery is automatic in the common case. When the plugin record is ready and only its worker is down — a crash in restart-backoff — Paperclip listens for the worker manager to respawn it and re-runs the setup the moment it comes back, so the environment returns without anyone restarting the server. A missing or non-ready plugin needs the next healthy boot instead.
That's the same deliberate split you see elsewhere in the document: a malformed section refuses startup, while a failed setup step logs and boots degraded.
Removing an entry doesn't remove the environment
Dropping the environments section, or the entry inside it, stops future refreshes — it does not delete or archive the environment row. This matches how plugins.autoInstall behaves (Plugins covers the plugin side): runs may still hold leases against that environment, so withdrawing it is an explicit action someone takes, not a side effect of an edited document. The archiving described above is scoped to a provider that was declared but couldn't come up, never to an entry you removed.
Secrets
| Variable | Meaning |
|---|---|
PAPERCLIP_SECRETS_MASTER_KEY |
32-byte encryption key as base64, hex, or raw |
PAPERCLIP_SECRETS_MASTER_KEY_FILE |
Path to the local key file |
PAPERCLIP_SECRETS_STRICT_MODE |
Require secret refs for server-side env bindings. Does not apply to paperclipai configure --section llm or config.llm.apiKey. |
These values are covered in more detail in Secrets.
Storage
| Variable | Meaning |
|---|---|
PAPERCLIP_STORAGE_PROVIDER |
Storage backend, usually local_disk or s3. |
PAPERCLIP_STORAGE_LOCAL_DIR |
Base directory for local-disk storage. |
PAPERCLIP_STORAGE_S3_BUCKET |
S3 bucket name. |
PAPERCLIP_STORAGE_S3_REGION |
S3 region. |
PAPERCLIP_STORAGE_S3_ENDPOINT |
Custom S3-compatible endpoint for MinIO, R2, and similar providers. |
PAPERCLIP_STORAGE_S3_PREFIX |
Optional object key prefix. |
PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE |
Enable path-style S3 requests when the provider needs them. |
PAPERCLIP_IMPORT_ZIP_MAX_BYTES |
Maximum size, in bytes, of a company-import .zip upload. Defaults to 1073741824 (1 GB). Set it higher to accept larger packages, or lower to tighten the cap. Values are clamped to the range 1 byte – 64 GiB; anything non-numeric, fractional-to-zero, or out of range falls back to the 1 GB default. |
Scheduler
| Variable | Default | Meaning |
|---|---|---|
HEARTBEAT_SCHEDULER_ENABLED |
true |
Enables or disables timer-based scheduling. |
HEARTBEAT_SCHEDULER_INTERVAL_MS |
30000 |
Scheduler poll interval in milliseconds. |
Telemetry & Feedback Export
These variables control where the server forwards operator-submitted feedback (and the deprecated telemetry channel that backs the same export pipeline). They are read by server/src/config.ts and are only consulted when you want to ship feedback events off your instance to a separate collector.
| Variable | Default | Meaning |
|---|---|---|
PAPERCLIP_FEEDBACK_EXPORT_BACKEND_URL |
unset | URL of the external feedback collector. When set, the server forwards paperclipai feedback submissions to this endpoint. |
PAPERCLIP_FEEDBACK_EXPORT_BACKEND_TOKEN |
unset | Bearer token used to authenticate the forwarding request. |
PAPERCLIP_TELEMETRY_BACKEND_URL |
unset | Legacy alias for PAPERCLIP_FEEDBACK_EXPORT_BACKEND_URL. Honoured for backwards compatibility — set the feedback variant in new deployments. |
PAPERCLIP_TELEMETRY_BACKEND_TOKEN |
unset | Legacy alias for PAPERCLIP_FEEDBACK_EXPORT_BACKEND_TOKEN. |
If neither variable is set, feedback submissions are stored locally and never leave the instance.
Turning off telemetry
Paperclip ships anonymized usage telemetry (which adapters you run, and similar coarse signals) that helps the project understand how Paperclip is used. It carries no personal data, and you can turn it off completely by setting either of these variables. Both are checked for the exact value 1, and either one on its own disables telemetry.
| Variable | Meaning |
|---|---|
PAPERCLIP_TELEMETRY_DISABLED |
Set to 1 to disable Paperclip's anonymized telemetry. |
DO_NOT_TRACK |
The cross-tool Console Do Not Track convention. Set to 1 to disable telemetry. |
Observability (OpenTelemetry)
Paperclip can emit distributed traces over OpenTelemetry (OTLP) so you can watch requests flow through the server in a tracing backend like Jaeger, Tempo, or Honeycomb. It is opt-in and off by default — nothing is loaded until you point it at a collector.
To turn it on, set OTEL_EXPORTER_OTLP_ENDPOINT and install the OpenTelemetry packages the server needs (@opentelemetry/sdk-node, @opentelemetry/auto-instrumentations-node, the exporter for your protocol, @opentelemetry/resources, and @opentelemetry/semantic-conventions). If the endpoint is set but the packages are missing, the server logs a one-line hint and keeps running without tracing.
| Variable | Default | Meaning |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
unset | OTLP collector endpoint. Setting it is the master switch that enables tracing. |
OTEL_EXPORTER_OTLP_PROTOCOL |
grpc |
Exporter protocol: grpc, http/protobuf, or http/json. An unknown value logs a warning and falls back to grpc. |
OTEL_SERVICE_NAME |
paperclip |
Service name reported on spans. |
OTEL_SERVICE_VERSION |
unknown |
Service version reported on spans. |
A bad endpoint or an unreachable collector never takes the server down — the SDK logs the failure and tracing simply stays off. On shutdown the server flushes buffered spans (with a short timeout) before exiting.
Error Monitoring (Sentry)
Paperclip can report server-side errors to Sentry. It is opt-in and off by default, and everything about it fails open — no DSN, no package, no Sentry, no problem.
To turn it on, set SENTRY_DSN and install the optional @sentry/node peer dependency. The DSN also rides the authenticated session response to the browser, so the web UI reports its own errors (behind a React error boundary) once the server has one configured.
| Variable | Default | Meaning |
|---|---|---|
SENTRY_DSN |
unset | Sentry DSN. Setting it, with @sentry/node installed, enables server-side error capture and hands the DSN to the browser for client-side reporting. Unset means no Sentry is loaded at all. |
Operator Settings Controls
If you host Paperclip for other people, these two variables let you shape which settings surfaces your users see and what the defaults are. Both are read at boot, applied at read time, and never persisted — clearing the variable restores stock behavior. With both unset, the UI and API behave exactly as they did before these controls existed. See Instance Settings for the full walkthrough.
| Variable | Default | Meaning |
|---|---|---|
PAPERCLIP_HIDDEN_SETTINGS |
unset | Comma-separated list of settings-surface keys to hide from the UI — instance pages, company pages (Members, Invites, Secrets, Export, Import), individual sections and tabs, and individual experimental flags. Some keys also floor their mutation API with a 403; the rest hide UI only. Use instance.experimental.* to hide every experimental toggle, and add !instance.experimental.<key> entries to keep specific ones visible. Unknown keys are warned about and ignored, so one list can roll across a fleet of mixed app versions. |
PAPERCLIP_SETTING_DEFAULTS |
unset | JSON object that replaces the schema default of selected instance settings, e.g. {"feedbackDataSharingPreference":"allowed"}. An explicit user choice always wins; only values still sitting at the schema default resolve to yours. Malformed JSON or an invalid value for a known field stops the server from booting; unknown field names are warned about and ignored. |
Agent Runtime
The server injects these variables into agent processes when it starts a run:
| Variable | Always set? | Meaning |
|---|---|---|
PAPERCLIP_AGENT_ID |
yes | Agent ID. |
AGENT_HOME |
supported managed runs | Writable copy of the agent's persistent files. It is separate from the task workspace, HOME, and CODEX_HOME; see Agent files. |
PAPERCLIP_COMPANY_ID |
yes | Company ID. |
PAPERCLIP_API_URL |
yes | Paperclip API base URL. |
PAPERCLIP_API_KEY |
local adapters | Short-lived JWT for API auth. Use as Authorization: Bearer $PAPERCLIP_API_KEY. For non-local adapters, the operator sets this in adapter config. |
PAPERCLIP_RUN_ID |
yes | Current heartbeat run ID. Pass back as the X-Paperclip-Run-Id header on any request that mutates an issue, so server-side audit log entries link to this run. |
PAPERCLIP_TASK_ID |
wake-driven | Issue that triggered the wake. Empty for scheduled or unsolicited wakes. |
PAPERCLIP_WAKE_REASON |
wake-driven | Why this run was triggered. See enum below. |
PAPERCLIP_WAKE_COMMENT_ID |
comment wakes | Specific comment that triggered the wake (set with issue_commented). |
PAPERCLIP_WAKE_PAYLOAD_JSON |
retired | No longer set. The wake context (the issue summary and the new comments) now travels in the run prompt instead, because a copy in the environment could exceed the operating system's process-launch limits. Paperclip also rejects this key if you put it in an adapter's env config. |
PAPERCLIP_APPROVAL_ID |
approval wakes | Resolved approval ID. |
PAPERCLIP_APPROVAL_STATUS |
approval wakes | Approval decision. |
PAPERCLIP_LINKED_ISSUE_IDS |
optional | Comma-separated linked issue IDs. |
Use these values when your agent runtime needs to authenticate back to Paperclip or understand what context triggered the run.
PAPERCLIP_WAKE_REASON values
| Value | When it fires |
|---|---|
issue_assigned |
A task was newly assigned to this agent. |
issue_commented |
A new comment was posted on an issue this agent owns. The triggering comment id is in PAPERCLIP_WAKE_COMMENT_ID. |
issue_comment_mentioned |
Retired. @-mentions are context only and no longer wake the mentioned agent, so new runs don't carry this reason; a wake request that still uses it is ignored. You may still see it on older runs. |
issue_blockers_resolved |
Every issue listed in this issue's blockedBy reached done. |
issue_children_completed |
All direct children of this issue reached a terminal state (done or cancelled). |
approval_resolved |
An approval the agent requested was approved or rejected. PAPERCLIP_APPROVAL_ID and PAPERCLIP_APPROVAL_STATUS are populated. |
scheduled |
A scheduled run from the heartbeat scheduler or a routine cron. |
assignment |
Generic assignment-triggered run with no more specific reason. |
When Paperclip realizes an execution workspace, it can also inject workspace-specific variables such as:
PAPERCLIP_WORKSPACE_CWDPAPERCLIP_WORKSPACE_PATHPAPERCLIP_WORKSPACE_REPO_ROOTPAPERCLIP_WORKSPACE_BRANCHPAPERCLIP_PROJECT_IDPAPERCLIP_ISSUE_ID
Those are mainly useful for adapter authors and agent-side tooling that need direct access to the resolved execution workspace.
Audit trail: Every mutating API request from an agent run should include the
X-Paperclip-Run-Id: $PAPERCLIP_RUN_IDheader. The server uses it to attribute issue updates, comments, checkouts, and subtasks to the heartbeat run that produced them. Read-only requests do not require it.
How PAPERCLIP_* env bindings reach the run
You can define your own environment variables on an agent, project, routine, or adapter config, and they flow into the run environment — including ones you deliberately name with a PAPERCLIP_ prefix (for example a secret called PAPERCLIP_CLOUD_PROD_PROVIDER_RAILWAY_TOKEN). The server applies one simple policy when it builds each run's environment:
PAPERCLIP_API_KEYis never accepted from your agent, project, routine, or adapter config. The harness mints a short-lived run token for every run, and that minted token is the only source ofPAPERCLIP_API_KEY.- Harness-assigned runtime variables always win. The variables Paperclip sets for the run itself (like
PAPERCLIP_RUN_ID,PAPERCLIP_AGENT_ID, and the wake/workspace variables above) take precedence over any same-named binding you define, so you cannot accidentally shadow them. - Every other
PAPERCLIP_*binding flows through to the run environment just like a variable with no prefix. Naming a value with thePAPERCLIP_prefix no longer causes it to be dropped.
Upgrade note (v2026.722.0): Earlier releases stripped every
PAPERCLIP_-prefixed binding before resolving the run environment, which silently dropped custom secrets you had named with that prefix — those now pass through as expected. In the same change, a staticPAPERCLIP_API_KEYyou set in adapter or config env to override the run token no longer has any effect. If you relied on that override, drop it: the harness-minted run token is now the only source of the run API key.
LLM Provider Keys
| Variable | Meaning |
|---|---|
ANTHROPIC_API_KEY |
Anthropic API key for claude_local |
OPENAI_API_KEY |
OpenAI API key for codex_local |
GEMINI_API_KEY |
Gemini API key for gemini_local |
CODEX_API_KEY |
Accepted in place of OPENAI_API_KEY for codex_local when it runs through ACP |
GOOGLE_API_KEY |
Alternate Google API key path for gemini_local |
Tip: If an adapter test is failing, start by checking whether the expected provider key is present in the process environment.
Adapter Provider Overrides
The local CLI adapters can be pointed at a custom or remote OpenAI-compatible gateway through these server-read variables. Each takes a JSON value that Paperclip writes into the adapter's own runtime config before a run, so you can route an adapter at your own provider without editing the agent's machine by hand. The full JSON shape and behaviour for each live on the adapter's reference page.
| Variable | Adapter | Meaning |
|---|---|---|
PAPERCLIP_CODEX_PROVIDERS |
codex_local |
JSON of custom providers (and an optional model_provider) written into Codex's managed config.toml. See Codex. |
PAPERCLIP_PI_PROVIDERS |
pi_local |
JSON of custom providers written into Pi's managed models.json. See Pi. |
PAPERCLIP_OPENCODE_PROVIDERS |
opencode_local |
JSON merged into OpenCode's provider config. See OpenCode. |
PAPERCLIP_OPENCODE_SMALL_MODEL |
opencode_local |
Sets OpenCode's small_model (the auxiliary/helper model). See OpenCode. |
Values support {env:VAR} placeholders, which are expanded server-side so secrets stay out of the stored JSON.
Adding models to the model picker
If an adapter's model dropdown is missing a model you can actually run — for example a model behind your own gateway that the server can't discover — set PAPERCLIP_ADAPTER_MODELS on the Paperclip host. Its value is a JSON object that maps an adapter type to the list of models to offer:
{ "codex_local": [{ "id": "my-gateway-model", "label": "My gateway model" }] }
Each entry needs a non-empty id; label is optional and defaults to the id. When an adapter has a list here, the dropdown — and its refresh control — shows exactly that list instead of the adapter's built-in or discovered models. Malformed JSON is logged and ignored, so the pickers fall back to their normal lists.
Paperclip Cloud Connector (Google OAuth broker)
These variables connect your instance to the Paperclip Cloud broker, which brokers Gmail and Google Workspace sign-ins for your agents. Most of the time you don't need any of them: on a self-hosted instance, enrolling from the Apps setup generates the instance keys and stores them for you, and Cloud-hosted instances receive their values automatically. Set them only when you manage the instance identity yourself, and keep both private keys in your deployment's secret manager rather than in plain config.
The instance ID, both private keys, and the environment work as a set. Once you set any of the three identity values, all four have to be present and valid, or the server rejects the configuration.
| Variable | Meaning |
|---|---|
PAPERCLIP_CLOUD_CONNECTOR_BASE_URL |
Broker base URL. Defaults to https://my.paperclip.app. Must use HTTPS; plain http:// is accepted only for loopback hosts. URLs carrying userinfo, a query string, or a fragment are rejected. |
PAPERCLIP_CLOUD_CONNECTOR_ENVIRONMENT |
Broker environment. One of development, staging, or production; any other value is rejected. It has to match the broker — https://my.paperclip.app is production and https://my-staging.paperclip.app is staging. |
PAPERCLIP_CLOUD_CONNECTOR_INSTANCE_ID |
Instance identifier issued when you enroll with Paperclip Cloud. |
PAPERCLIP_CLOUD_CONNECTOR_SIGN_PRIVATE_KEY |
Private Ed25519 signing key for connector requests. Keep it in your secret manager. |
PAPERCLIP_CLOUD_CONNECTOR_SEAL_PRIVATE_KEY |
Private X25519 sealing key for connector payloads. Keep it in your secret manager. |
Retired: PAPERCLIP_ID_CONNECTOR_*
| Variable | Status |
|---|---|
PAPERCLIP_ID_CONNECTOR_BASE_URL |
retired |
PAPERCLIP_ID_CONNECTOR_ENVIRONMENT |
retired |
PAPERCLIP_ID_CONNECTOR_INSTANCE_ID |
retired |
PAPERCLIP_ID_CONNECTOR_SIGN_PRIVATE_KEY |
retired |
PAPERCLIP_ID_CONNECTOR_SEAL_PRIVATE_KEY |
retired |
These belonged to the older Paperclip ID broker, which used a different, incompatible protocol. They are not aliases for the PAPERCLIP_CLOUD_CONNECTOR_* variables above. If an instance still sets them and has no Paperclip Cloud enrollment, the Google connector reports CONNECTOR_MIGRATION_REQUIRED. To fix it, enroll the instance with Paperclip Cloud, remove the old values, and reconnect each Google account that was linked through the old broker.
HTTP Adapter Private Endpoint Allowlist
By default, HTTP adapters may call public HTTP(S) origins but not private ones — the socket-level guard rejects private networks and link-local metadata targets and pins DNS results to prevent rebinding. Use this variable to opt specific trusted private origins into HTTP-adapter calls.
| Variable | Default | Meaning |
|---|---|---|
PAPERCLIP_HTTP_ADAPTER_PRIVATE_ENDPOINT_ALLOWLIST |
unset (public origins only) | Comma-separated list of exact origins that may be reached over private networking. Each entry is a full origin — scheme, host, and port, like http://hooks.internal.example:8080 — matched case-insensitively. Entries must be http or https, carry no path beyond /, and no userinfo, query string, or fragment; anything else in the list is ignored. |