CONCEPTS · WORKSPACES & CONFIG LAYERS

Workspaces & config layers

Since Synthex 2.3.0, settings can live in four places: your user config, the project's shared config, your own uncommitted overrides for one repo, and the plugin's defaults. The team pins what matters in the repo, you keep personal preferences out of it, and two safety rules make sure a repo can't override what your machine has or switch on external review you turned off.

The four layers

Highest precedence first:

LayerPathScope
Project-local.synthex/config.local.yamlYour settings for this one repo. Gitignored by init/synthex:init, never committed.
Project (shared).synthex/config.yamlThe team's settings for this repo. Committed.
User~/.config/synthex/config.yamlYour settings for every repo. Honors $XDG_CONFIG_HOME.
Plugin defaultsconfig/defaults.yamlShipped with Synthex.

Put only the keys you want to change in each file. Anything you leave out falls through to the next layer. With no config files at all, every command runs on the plugin defaults.

Project settings beat user settings. This matches Claude Code's settings precedence: a repo can pin behavior for everyone who works in it.

A legacy .synthex-plus/config.yaml is still read between the project and user layers for one major version. When a key resolves only there, Synthex prints a deprecation notice.

Two exceptions

Two narrow sets of keys don't follow that order.

User-owned: multi_model_review.per_reviewer.*

Which external models are installed on a machine, and what they are called, is a fact about the machine, not the repo. Synthex checks the user layer first for these keys. A repo that asks for qwen2.5-coder:32b can't override the model you actually pulled.

Restrictive wins: multi_model_review.enabled and multi_model_review.*.enabled

false in any layer you wrote disables the feature. This works the way Claude Code combines permission deny rules instead of letting one layer replace another. A repo can't switch on sending your code to an external CLI when you have switched it off. Plugin defaults don't count here, so the shipped false is still only a default that a repo can turn on.

Example: a personal kill switch

Put this in your user config to disable external multi-model review in every repo, whatever a repo's config asks for:

# ~/.config/synthex/config.yaml
multi_model_review:
  enabled: false

Example: a personal override for one repo

Your project-local file is for preferences the team shouldn't inherit. For example, to use cloud workers for next-priority/synthex:next-priority in one repo without changing the committed config:

# .synthex/config.local.yaml (gitignored)
next_priority:
  execution_environment: cloud

What init sets up

init/synthex:init creates .synthex/config.yaml and the document directories (docs/reqs/, docs/plans/, docs/specs/, and so on). It also adds these per-developer paths to .gitignore if they are missing:

# Synthex worktrees (parallel task execution)
.claude/worktrees/

# Synthex per-developer state (upgrade-nudge tracking)
.synthex/state.json

# Synthex native-looping state (per-session, per-developer)
.synthex/loops/

# Synthex cached project facts (per-clone SHAs and mtimes)
.synthex/facts.md

# Synthex project-local config (one developer's overrides for this repo)
.synthex/config.local.yaml

The worktrees entry uses your worktrees.base_path if you changed it. Running init is optional: every command works without it.

What lives in .synthex/

Only config.yaml is meant to be committed. The rest belongs to one developer or one clone:

PathWhat it holds
config.yamlThe team's shared settings. Committed.
config.local.yamlYour overrides for this repo. Gitignored.
state.jsonUpgrade-nudge tracking (last seen version, dismissal) and the installed plugin_root some hosts use to find the plugin. Gitignored.
loops/<loop-id>.jsonState for each native loop, so loops can run side by side and resume across sessions. Gitignored.
facts.mdProject facts init detects (commit convention, test runner, frontend framework, spec index) with SHAs and mtimes for freshness checks. Gitignored.
auto-decide/<session_id>.jsonlA log of each decision --auto-decide took on Claude Code, with the commit before it.

Worktrees: isolated workspaces on your machine

When next-priority/synthex:next-priority runs tasks in parallel locally, each task gets its own git worktree: a separate working directory on its own branch, sharing the same repository.

worktrees:
  base_path: .claude/worktrees # relative to the repo root; absolute paths work too
  branch_prefix: "feature/"

For each task, Synthex runs:

git worktree add {base_path}/{branch_prefix}[task-id]-[short-description] \
  -b {branch_prefix}[task-id]-[short-description]

The worker commits only on its own branch. After the task passes validation, Synthex merges it with git merge --ff-only, falling back to a merge commit when a fast-forward isn't possible, and removes that worktree right away, before it touches the next task. It never force-removes a worktree that has uncommitted changes.

init also creates a committed .worktreeinclude file (starting with .env and .env.local). Claude Code's built-in worktree support reads it to copy those gitignored files into each new worktree it creates.

Cloud agents: a VM instead of a worktree

On Cursor, cloud execution replaces local worktrees entirely. Each worker gets its own cloud VM, creates the same {branch_prefix}[task-id]-[short-description] branch from the remote base, and pushes it. Your session fetches the branch and merges it with git merge --ff-only FETCH_HEAD. Because cloud workers clone from the remote, commit and push first. See Cloud agents for the full lifecycle.

Next