Skip to content

Configuration

FineCode merges configuration from multiple sources in order of increasing priority:

preset.toml  →  pyproject.toml / finecode.toml  →  finecode-user.toml  →  environment variables  →  CLI flags

Higher-priority sources override lower-priority ones.

Where configuration lives

Scope File Notes
Workspace finecode-workspace.toml at the workspace root Only valid location for workspace-scoped settings
Project [tool.finecode.*] in pyproject.toml Default; recommended
Project finecode.toml at the project root Alternative to pyproject.toml; cannot be combined with it
User (project) finecode-user.toml at the project root Personal preferences; gitignored; optional
User (preset) finecode-user.toml next to preset.toml Editable presets only; gitignored; optional

finecode.toml and [tool.finecode.*] in pyproject.toml are mutually exclusive within a project — pick one. finecode.toml uses a [finecode] top-level table; the structure is otherwise identical. A lone finecode.toml without an adjacent pyproject.toml is ignored.

pyproject.toml

All FineCode project configuration lives under [tool.finecode].

Enabling presets

[tool.finecode]
presets = [
    { source = "fine_python_recommended" },
    { source = "my_custom_preset" },
]

Presets are applied in order. Later presets' handlers are added after earlier ones.

Declaring actions and handlers

You can declare or extend actions directly in your project:

[tool.finecode.action.lint]
source = "finecode_extension_api.actions.lint.LintAction"
handlers = [
    { name = "ruff", source = "fine_python_ruff.RuffLintFilesHandler", env = "dev_no_runtime", dependencies = ["fine_python_ruff~=0.2.0"] },
    { name = "mypy", source = "fine_python_mypy.MypyLintFilesHandler", env = "dev_no_runtime", dependencies = ["fine_python_mypy~=0.3.0"] },
]

Replacing preset handlers

To completely replace the handlers from presets for an action:

[tool.finecode.action.lint]
source = "finecode_extension_api.actions.lint.LintAction"
handlers_mode = "replace"
handlers = [
    { name = "mypy", source = "fine_python_mypy.MypyLintFilesHandler", env = "dev_no_runtime", dependencies = ["fine_python_mypy~=0.3.0"] },
]

Disabling a specific handler

[tool.finecode.action.lint]
handlers = [
    { name = "flake8", enabled = false },
]

Configuring a handler

By source path — use [[tool.finecode.action_handler]]:

[[tool.finecode.action_handler]]
source = "fine_python_ruff.RuffLintFilesHandler"
config.extend_select = ["B", "I"]
config.line_length = 100

[[tool.finecode.action_handler]]
source = "fine_python_flake8.Flake8LintFilesHandler"
config.max_line_length = 88
config.extend_ignore = ["E203", "E501"]

By action and handler name — use a table keyed by handler name under the action:

[tool.finecode.action.lint.handlers.ruff]
config.line_length = 100

[tool.finecode.action.lint.handlers.flake8]
config.max_line_length = 88
enabled = false

Any handler field can be overridden this way: config, enabled, env, dependencies, etc. The entry is merged into the handler already declared by the action (typically from a preset) — it does not replace it. This syntax is useful when you want to reference a handler by its logical name rather than its source class, for example to supply deployment-specific config for a handler that a preset declares.

Pinning extension tool versions

Each extension declares a compatibility range for the tool it wraps (see Creating an Extension — Tool versioning). To pin a specific version across every handler the extension contributes, configure the extension once by its package name:

[tool.finecode.extension.fine_python_ruff]
dependencies_override = ["ruff==0.9.0", "ruff-plugin-foo==1.2.3"]

For the rare case where you need different tool versions for different handlers from the same extension, fall back to a handler-level dependencies_override — it wins over the extension-level value for that handler.

Declaring services

Services are shared, long-lived dependencies used by handlers. Declare service bindings with [[tool.finecode.service]] entries:

[[tool.finecode.service]]
interface = "finecode_extension_api.interfaces.ilspclient.ILspClient"
source = "finecode_extension_runner.impls.lsp_client.LspClientImpl"
env = "dev_no_runtime"
dependencies = []

Service declarations are merged by interface. If a preset declares a service, you can rebind it in your project by declaring the same interface with a different source:

[[tool.finecode.service]]
interface = "finecode_extension_api.interfaces.ihttpclient.IHttpClient"
source = "my_company_http.MyHttpClient"
env = "dev_no_runtime"
dependencies = ["my_company_http~=1.2.0"]

Services can also declare config, structured into a config-annotated constructor parameter on the implementation — the same structured-dataclass pattern action handler config already uses (ADR-0056). The built-in ICommandRunner service (every subprocess an ER spawns, e.g. uv install, goes through it) is the worked example: it takes a max_concurrent_processes setting that bounds prepare-envs's per-ER subprocess fan-out (ADR-0055; see Bounding concurrency for the full picture, including the companion cross-project cap).

[[tool.finecode.service]]
interface = "finecode_extension_api.interfaces.icommandrunner.ICommandRunner"
source = "finecode_extension_runner.impls.command_runner.CommandRunner"
env = "dev_no_runtime"
config.max_concurrent_processes = 4

Because a concurrency cap like this is bound to the machine running FineCode rather than to the project, prefer declaring it in a personal, gitignored finecode-user.toml instead of a committed pyproject.toml — a value tuned for one developer's machine would be wrong on a teammate's differently-sized one.

Configuring Extension Runner logging

Each Extension Runner is a separate subprocess. Its log level and per-group overrides are configured under [tool.finecode.er]. The WM reads this at startup and delivers the resolved config to the ER — the ER never reads config files directly.

[tool.finecode.er.logging] is the project-level fallback that applies to all ERs in the project. Per-env overrides live under [tool.finecode.er.envs.<env_name>.logging] and merge additively with the fallback.

# project-level fallback — applies to all ERs
[tool.finecode.er.logging]
default_level = "INFO"

[tool.finecode.er.logging.log_groups]
"finecode_extension_runner" = "WARNING"

# per-env override — applies only to the dev_no_runtime ER
[tool.finecode.er.envs.dev_no_runtime.logging]
default_level = "DEBUG"

[tool.finecode.er.envs.dev_no_runtime.logging.log_groups]
"fine_python_ruff" = "TRACE"

Env var overrides (highest priority):

Variable Effect
FINECODE_ER_LOG_LEVEL project-level default_level
FINECODE_ER_ENV_<ENV>_LOG_LEVEL per-env default_level (<ENV> uppercased, -_)
FINECODE_ER_LOG_GROUP_<GROUP> project-level log_groups entry (<GROUP> uppercased, ._)
FINECODE_ER_ENV_<ENV>_LOG_GROUP_<GROUP> per-env log_groups entry

finecode.toml

finecode.toml is an alternative location for project-scoped configuration. It uses a [finecode] top-level table instead of [tool.finecode]; every sub-table and field name is identical otherwise. The two files are mutually exclusive — if both exist for the same project, FineCode raises an error at startup.

[finecode]
presets = [{ source = "fine_python_recommended" }]

[finecode.action.lint]
source = "finecode_extension_api.actions.lint.LintAction"
handlers = [
    { name = "ruff", source = "fine_python_ruff.RuffLintFilesHandler", env = "dev_no_runtime", dependencies = ["fine_python_ruff~=0.2.0"] },
]

[[finecode.action_handler]]
source = "fine_python_ruff.RuffLintFilesHandler"
config.line_length = 100

[[finecode.service]]
interface = "finecode_extension_api.interfaces.ihttpclient.IHttpClient"
source = "finecode_httpclient.HttpClient"
env = "dev_no_runtime"
dependencies = ["finecode_httpclient~=0.1.0a1"]

The [workspace] table is not allowed in finecode.toml; workspace-scoped settings always go in finecode-workspace.toml.

finecode-user.toml

finecode-user.toml is a personal, gitignored configuration layer for developer-specific preferences that must not affect the shared workspace configuration. A motivating example is installing a personal AI assistant inside a devcontainer: different developers may want Copilot, Codeium, or nothing at all, and none of these choices belong in a committed config file.

File locations

There are two optional locations, following a uniform sibling rule: every project-level config file and every preset.toml gets an optional sibling finecode-user.toml in the same directory.

Location Scope
{project-root}/finecode-user.toml Personal project-level preferences; merged into that project's resolved config above project config
{preset-dir}/finecode-user.toml Merged into that preset's config at read time; sits at preset priority

finecode-workspace.toml does not get a sibling — workspace-scoped settings are shared by definition.

Schema

finecode-user.toml uses the same fields as finecode.toml but with no [finecode] wrapper — tables are written at the top level:

presets = [{ source = "my_personal_preset" }]

[action.setup_dev_tools]
handlers = [
    { name = "copilot", source = "fine_vscode_ext.InstallExtHandler", config.ext_id = "GitHub.copilot" },
]

[action.lint.handlers.ruff]
config.line_length = 90

[[service]]
interface = "myext.IMyService"
source = "myext.MyServiceImpl"
env = "dev_no_runtime"
dependencies = []

[dependency-groups]
dev_workspace = ["my_personal_preset>=1.0"]

[workspace] is not allowed in any user file — workspace settings always go in finecode-workspace.toml.

What it can do

Everything finecode.toml can do: declare presets, add handlers to actions, configure existing handlers (config, enabled, env, dependencies), replace handler lists (handlers_mode = "replace"), declare services, configure the Extension Runner ([er]).

One capability beyond finecode.toml: user files may declare a [dependency-groups] section to add packages to any dependency group (see below).

Adding dependency groups

[dependency-groups]
dev_workspace = ["my_personal_preset>=1.0"]

User-declared groups are merged additively into the project's groups — existing packages are not removed. This is the primary mechanism for installing a personal preset package into dev_workspace so prepare-envs picks it up automatically.

Handler dependencies fields still work as normal; [dependency-groups] is only needed when installing an entire personal preset package that is not part of the shared config.

Declaring personal presets

presets = [{ source = "my_personal_preset" }]

[dependency-groups]
dev_workspace = ["my_personal_preset>=1.0"]

User-declared presets are resolved in the same pass as shared presets and sit at preset priority — project config, workspace config, and the project-level user file can all override them.

The preset package must be importable from dev_workspace. If it is not part of the shared config, declare it under [dependency-groups] in the same user file and prepare-envs will install it.

First-run two-step. On the first prepare-envs after adding a personal preset this way, the package is installed in dev_workspace but the preset's own handler dependencies cannot yet be resolved (the package was unavailable during that run). Run prepare-envs a second time to install those. This is the same behavior as adding any new preset to shared config.

Merge priority

(preset + preset-user, merged at read time)
  → project (pyproject.toml / finecode.toml)
  → workspace (finecode-workspace.toml)
  → project-user  ({project-root}/finecode-user.toml)
  → env vars
  → CLI flags

Project-level user config wins over all file-based shared config. Preset-level user config sits at preset priority and can be overridden by project config, workspace config, and the project-level user file.

Preset-level user config — limitations

Editable presets only. The preset-level user file is only practical for presets that are editable installs with a stable on-disk directory (e.g. local monorepo presets). For presets installed as non-editable packages, the preset directory lives inside the virtualenv and is overwritten by prepare-envs. Use the project-level user file for non-editable presets.

No [dependency-groups] at preset level. Dependency group declarations in a preset-level user file are not supported and generate a warning. Declare personal packages in the project-root finecode-user.toml instead.

gitignore convention

Add finecode-user.toml to your .gitignore:

finecode-user.toml

The file is gitignored by convention; FineCode does not enforce this. If the file is absent, all behavior is a safe no-op.

Example

Shared project config (pyproject.toml) — committed to git:

[tool.finecode.action.setup_dev_tools]
source = "finecode_extension_api.actions.setup_dev_tools.SetupDevToolsAction"
handlers = []

Developer A's project-root finecode-user.toml — gitignored:

presets = [{ source = "my_personal_preset" }]

[action.setup_dev_tools]
handlers = [
    { name = "copilot", source = "fine_vscode_ext.InstallExtHandler", config.ext_id = "GitHub.copilot" },
]

[dependency-groups]
dev_workspace = ["my_personal_preset>=1.0"]

Developer B's project-root finecode-user.toml:

[action.setup_dev_tools]
handlers = [
    { name = "codeium", source = "fine_vscode_ext.InstallExtHandler", config.ext_id = "Codeium.codeium" },
]

.devcontainer/devcontainer.json — shared, identical for everyone:

{
  "postCreateCommand": "finecode run setup_dev_tools"
}

For developers without a finecode-user.toml, the action runs with no handlers and is a no-op. For developers who have added handlers, their tools are installed automatically on devcontainer rebuild.

finecode-workspace.toml

Workspace-level configuration lives in finecode-workspace.toml at the workspace root, under the [workspace] table.

Workspace editable packages

In a monorepo, local packages should be installed as editable installs. Declare them once in finecode-workspace.toml:

[workspace]
# When true, every project discovered in this workspace is automatically
# installed as an editable install when it appears as a dependency.
all_workspace_packages_editable = true

# Optional: explicit paths to treat as editable installs — useful for
# vendored forks outside normal project discovery. Paths are relative to
# the workspace root.
editable_packages = [
    "./vendored_forks/some_lib",
]

Any dependency whose package name matches a workspace editable package is automatically rewritten to an editable install from its declared path, across every env in every project. The resolved set is the union of every discovered project (when all_workspace_packages_editable is true) and every explicit editable_packages entry.

WM telemetry

Configure the OTLP endpoint for the Workspace Manager and all Extension Runners under [workspace.wm.telemetry]:

[workspace.wm.telemetry]
otlp_endpoint = "http://localhost:4317"

The FINECODE_OTLP_ENDPOINT environment variable overrides this value (higher priority).

The endpoint must include an explicit host and port; a malformed value fails fast at startup. It does not need to be reachable when FineCode starts: exporters buffer and retry, so a backend brought up later is picked up without a restart. An unreachable endpoint produces a single startup heads-up rather than an error stream.

Point this at any OTLP-compatible backend. For guidance on running one locally — including a ready-to-run single-container option — see the Observability guide.

WM logging

The Workspace Manager process reads its per-group log level overrides from [workspace.wm.logging]. This section controls only the WM process — it has no effect on ERs.

[workspace.wm.logging.log_groups]
"finecode.wm_server.runner.runner_manager" = "DEBUG"
"finecode_jsonrpc.client" = "TRACE"

The overall default log level is set via the --log-level CLI flag (default INFO). Env vars override the file config:

FINECODE_WM_LOG_GROUP_FINECODE_WM_SERVER_RUNNER_RUNNER_MANAGER=DEBUG
FINECODE_WM_LOG_GROUP_FINECODE_JSONRPC_CLIENT=TRACE

(<GROUP> is uppercased with ._.)

To surface WM and ER logs in a CI job log (rather than only in files), pair --log-level with --verbose — see Diagnostic logs in CI for the recommended INFO-by-default, DEBUG-on-demand recipe.

Environment variables

Override handler config at runtime without modifying files.

Format:

FINECODE_CONFIG_<ACTION>__<PARAM>=<json_value>
FINECODE_CONFIG_<ACTION>__<HANDLER>__<PARAM>=<json_value>
  • <ACTION>, <HANDLER>, <PARAM> are uppercase, separated by double underscores (__)
  • Values are parsed as JSON (use "true", 123, "string", ["a","b"], etc.)

Examples:

# Set line_length for all handlers of the lint action
FINECODE_CONFIG_LINT__LINE_LENGTH=100 python -m finecode run lint

# Set line_length only for the ruff handler
FINECODE_CONFIG_LINT__RUFF__LINE_LENGTH=120 python -m finecode run lint

# Pass a JSON array
FINECODE_CONFIG_LINT__RUFF__EXTEND_SELECT='["B","I"]' python -m finecode run lint

To disable env var config entirely:

python -m finecode run --no-env-config lint

CLI config flags

Override config inline on the command line. CLI flags take precedence over env vars.

Format:

--config.<param>=<value>
--config.<handler>.<param>=<value>

Examples:

# Action-level: applies to all handlers
python -m finecode run lint --config.line_length=120

# Handler-specific
python -m finecode run lint --config.ruff.line_length=120

# Combined
python -m finecode run lint --config.ruff.line_length=120 --config.mypy.strict=true

# CLI overrides env vars (line_length will be 120)
FINECODE_CONFIG_LINT__RUFF__LINE_LENGTH=100 python -m finecode run lint --config.ruff.line_length=120

Inspecting resolved configuration

Dump the fully merged configuration for a project to a file:

python -m finecode dump-config --project=my_project
# Output written to finecode_config_dump/

This is useful for debugging config merging from multiple presets.