Skip to content

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:

  1. --config <path> — explicit override (absolute, or relative to the current directory).
  2. ./claude-coder.config.json — project-local, if it exists.
  3. ~/.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.

[
  { "name": "...", "transport": "sdk" },
  { "name": "...", "transport": "pty" }
]

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):

  • allowedTools is required for unattended use. An MCP tool call normally raises a permission prompt. repl/tui ask 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:

"env": {
  "NODE_ENV": "production",
  "DB_PASSWORD": { "secretRef": "DB_PASSWORD" }
}

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.