Profiles & Configuration¶
A profile is a named, reusable recipe that pins how a session runs: the transport, the working directory, the model, the permission mode, and any extra config. This page covers the config file, the full profile schema, and how to inspect profiles.
The config file¶
Profiles live in a JSON file. claude-coder resolves the path in this order:
--config <path>— explicit override (absolute, or relative to the current directory)../claude-coder.config.json— project-local, if it exists.~/.claude/claude-coder/profiles.json— the home default.
The first match wins. If no file is found at the default path, claude-coder runs with built-ins only — there is one built-in profile, claude-code-expert (transport sdk).
The file is either a bare array of profiles, or a { "profiles": [...] } wrapper. Both load to the same thing.
A missing --config file is an error
A missing file at the default path is fine (you just get the built-ins). But if you pass --config and the file does not exist — or any file is malformed, has a profile without a string name, or a transport that is not sdk/pty — the command fails with a clear error and exit code 1.
Profile schema¶
Every field except name and transport is optional. The fields below are the complete set (from src/fleet/profile.ts).
| Field | Type | Description |
|---|---|---|
name |
string |
Required. Unique profile name. |
transport |
"sdk" \| "pty" |
Required. Which transport drives Claude Code. See concepts. |
cwd |
string |
Default working directory. A resolved cwd is required at session-create; supply it here, via --cwd, or let the CLI default it to the current directory. |
model |
string |
Claude model to request. |
permissionMode |
string |
SDK permission mode, e.g. default, acceptEdits, plan, bypassPermissions. |
permissionPolicy |
function | A code-only callback for per-request decisions. Not expressible in JSON; library use only. |
permissionTimeoutMs |
number |
How long to wait on a permission request. Default action on expiry is deny. |
systemPromptAppend |
string |
Text appended to the base system prompt. |
config |
object | Transport config passthrough: env, settings, mcpConfig, allowedTools, disallowedTools, addDirs. See below. |
emitRaw |
boolean |
Emit raw transport events. Off by default. |
experimentalAskUserQuestionAnswering |
boolean |
SDK only. Rejected at compile-time on pty. |
start |
object | Default session start. Defaults to { "kind": "new" }. |
extends |
string |
Inherit from another profile by name (single parent, cycle-guarded). |
The config block¶
config carries the per-session transport passthrough:
| Key | Type | Description |
|---|---|---|
env |
Record<string, EnvValue> |
Environment variables for the session. A value is a literal string or a { "secretRef": "..." } marker. See secrets. |
settings |
object | SDK flag-layer settings. |
mcpConfig |
object | MCP server config. |
allowedTools |
string[] |
Tool names to allow. |
disallowedTools |
string[] |
Tool names to deny. |
addDirs |
string[] |
Extra directories the session may access. |
Inheritance is shallow
extends merges field-by-field, child wins. Nested objects (env, settings, mcpConfig) are replaced wholesale, not deep-merged — a child that supplies its own env drops the parent's env entirely.
Wiring an MCP server (e.g. Context7)¶
config.mcpConfig is the MCP servers map — the same shape Claude Code's --mcp-config and the
Agent SDK's mcpServers expect (server name → server config). claude-coder injects it under strict
isolation (--strict-mcp-config / strictMcpConfig), so the session loads only the servers you
name here and ignores your ambient MCP config (see security).
For example, to give a profile Context7 for current, version-accurate library/framework docs (validated live, 2026-06):
{
"name": "expert",
"transport": "sdk",
"config": {
"mcpConfig": {
"context7": { "type": "http", "url": "https://mcp.context7.com/mcp" }
},
"allowedTools": ["mcp__context7__resolve-library-id", "mcp__context7__query-docs"]
}
}
A stdio server works the same way — { "command": "npx", "args": ["-y", "@upstash/context7-mcp"] }.
Any MCP server fits this pattern; keep the set small (a handful) so tool discovery stays sharp.
Context7 works keyless at public rate limits; pass an API key via the server's headers field
(see Context7's docs) if you need more.
Three behaviors to know (all observed live):
allowedToolsis required for unattended use. An MCP tool call normally raises a permission prompt.repl/tuiask you interactively, but an unattended surface (serve-api) has no answerer and an unanswered prompt parks the turn until teardown — so pre-allow the server's tools (mcp__<server>__<tool>) as above. Current Context7 tool names:resolve-library-id,query-docs.- Servers connect asynchronously (Context7: ~3–5 s). The session does not wait for them, so an instant first turn can race the connection; by the time a real turn invokes a tool the server is normally up.
- MCP tools are loaded on demand. The driven agent fetches MCP tool schemas lazily (deferred tool loading), so a naive "list your tools" prompt won't show them — just instruct it to use the server (e.g. "use context7 to look up …") and it loads the tools itself.
To re-validate end-to-end (spends ~1 turn): npx tsx scripts/live-e2e/diag-context7-mcp.mts.
Replacing the old knowledge layer
claude-coder no longer ships a built-in docs MCP. Wiring Context7 (or another docs MCP) into a profile is the recommended way to give a driven session current documentation — now for your whole stack, not just Claude Code's own docs.
Example config¶
Two profiles below: one sdk, one pty. A third extends the first.
[
{
"name": "reviewer",
"transport": "sdk",
"cwd": "/home/me/projects/app",
"model": "claude-opus-4-1",
"permissionMode": "default",
"systemPromptAppend": "Be terse. Prefer diffs over prose.",
"config": {
"env": {
"NODE_ENV": "production",
"GITHUB_TOKEN": { "secretRef": "GITHUB_TOKEN" }
},
"allowedTools": ["Read", "Grep", "Bash"]
}
},
{
"name": "terminal",
"transport": "pty",
"cwd": "/home/me/projects/app",
"permissionMode": "acceptEdits"
},
{
"name": "reviewer-sandbox",
"transport": "sdk",
"extends": "reviewer",
"cwd": "/tmp/scratch",
"config": {
"env": { "NODE_ENV": "development" }
}
}
]
config.env and secrets¶
config.env sets environment variables for the session. Each value is either a plain string or a secret reference:
A { "secretRef": "..." } is resolved at session-create by a SecretsProvider; the resolved plaintext is never written back to the config or persisted. The default provider resolves every reference to nothing (the key is dropped), so secrets are opt-in.
Credential env vars are rejected
Auth is always your Claude subscription (claude /login OAuth), never an API key. A profile that sets ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, AWS_BEARER_TOKEN_BEDROCK, or any other billed-credential / auth-redirect variable as a literal value is rejected at load time with exit code 1. A secretRef whose resolved value is a credential var is rejected later, at session-create time, when the SecretsProvider runs (see secrets).
For the full SecretRef mechanism, the SecretsProvider seam, and the credential guard, see secrets and security.
Inspecting profiles¶
The merged set of profiles (built-ins plus the config file) is available programmatically through FleetApi.listProfiles() and FleetApi.getProfile(name), which return serializable views. The views report the permissionPolicy only as hasPermissionPolicy: true|false (it is a closure, never serialized), and literal config.env values are redacted; secretRef markers pass through, since the reference name is not itself a secret. See the library guide for the in-process API.
Programmatic profiles¶
The JSON schema above is compiled by the profile engine, whose pieces are exported from the claude-coder barrel for library use: compileProfile (resolve a profile + overrides into a CompiledProfile), ProfileRegistry (the named-profile store), the ProfileConfig / ProfileOverrides / TransportKind types, and ConfigError (thrown on an invalid profile or a credential_in_profile violation — see secrets). The CLI, TUI, and serve-api gateway all build profiles through these.
See the CLI reference for how repl and tui consume a profile by name.