# Configuration Corresync uses strict, secret-free TOML: - Linux: `$XDG_CONFIG_HOME/corresync/config.toml`, normally `~/.config/corresync/config.toml`; - macOS: `~/Library/Application Support/corresync/config.toml`; - Windows: `%AppData%\corresync\config.toml`. Use `CORRESYNC_CONFIG` or global `--config` for an explicit file. The directory is protected and the file is atomically written with owner-only permissions where supported. Supported schema versions, fail-safe defaults, migration, downgrade refusal, and support-window rules are defined by the [public and local versioning policy](adr/0020-public-and-local-versioning.md). ## Prefer lifecycle commands ```console corr settings corr setup corr setup you@example.com --alias personal corr config init corr config validate corr config show corr account discover reader@example.invalid corr account add --help corr account list corr account show work corr account rename work primary corr account remove old --approve ``` Use `corr setup` for the resumable first-run coordinator: current-state preflight, optional user-local completion, the multi-account loop, multi-agent selection, reviewed integration setup, verification, and a Ready / Needs attention / Optional later summary. Use `corr settings` to open the same account registration flow alongside everyday settings. The settings form is organized around accounts, updates, safety, and browser login. It can add an account from the nested Accounts category, whose account list leads to either login mode, rename, default, and removal actions for one account. Its top-level Setup guide entry resumes the same complete coordinator used by `corr setup`; it does not duplicate completion or agent-host lifecycle logic. Removing an account requires an explicit local-state review. Removing the default requires selecting its replacement first, and the only configured account cannot be removed. Each row shows its current value, plain-language effect, and equivalent direct command. Set `CORRESYNC_ACCESSIBLE=true` for line-oriented screen-reader prompts and type `:cancel` to leave an input step safely. Direct `account` and `config` commands remain available for scripts and advanced configuration. Interactive `corr setup` creates an empty, secret-free configuration when needed, but derives every later step from authoritative local state instead of a completed flag. Account discovery is credential-free, chosen routes are previewed, and persistence happens only after confirmation. Authentication, external-credential verification, completion installation, and agent-host configuration remain separately visible choices. Host changes use the same previewed lifecycle as direct `corr integrations` commands and continue independently after a per-host failure. `corr setup ADDRESS` remains deterministic and non-interactive for scripts. `config init` creates only the empty configuration for users who want to inspect candidates and select every route manually. Discovery is read-only and credential-free. Adding a route never authenticates. The account receives a generated opaque ID, monitoring remains off, and login occurs only through `corr auth login --account ALIAS`. Saved mail and calendar queries are intentionally absent from `config.toml`. They are private account-local state under the opaque account state root, with owner-only permissions, a 64-definition and 256 KiB bound, strict decoding, atomic replacement, and revision-bound deletion. They never contain provider results or credentials. `corr queries purge --account ALIAS` is the explicit reviewed recovery path for a bounded malformed catalog. Removing the account purges the catalog with its other Corresync-owned state. Rename preserves the stable ID and every account-local state tree. Remove requires approval and deletes Corresync-owned profile, import, cursor, queue, and unshared Corresync-owned OAuth grant state. External standards credentials remain in their keyring/helper. Removing the default account requires `--new-default` when another account will remain. Removing the final account clears `default_account` and returns to the same provider-neutral empty configuration created by `corr config init`. ## Schema v11 Schema v11 adds an explicitly consented external credential reference for each user-owned Google Desktop OAuth client. A v10 Google route cannot acquire this new authority through migration and must follow the [legacy-route recovery](google-oauth-setup.md#recover-a-legacy-google-route) before it is re-added with `corr setup`; non-Google v10 routes migrate unchanged. Schema v10 added one optional, closed messaging route for Teams Graph, Teams Web, Slack, or Mattermost. Existing v9 files preserve every route and consent exactly; migration adds no messaging route, workspace, credential reference, monitoring consent, or runtime capability. Messaging remains release-gated before credential access, browser launch, or provider traffic until the full v0.9 evidence manifest is complete. Schema v9 added the typed TickTick task route with separate consented handles for its OAuth grant and externally owned confidential-client secret. Schema v8 added the independent Google Tasks OAuth payload. Schema v7 added the CalDAV VTODO payload, schema v6 introduced the task route, and existing v5 files still migrate with no task route, authorization, or capability. Schema v5 added the signed-release channel and existing older files retain their check and automatic-install consent. A freshly initialized provider-neutral configuration contains no account and has an empty `default_account`. The first account added becomes the default: ```toml version = 11 default_account = "" [policy] mode = "guarded" preview_sensitive_reads = false preview_reversible_writes = false max_recipients = 20 max_attendees = 50 [browser] login_timeout = "5m0s" [updates] channel = "stable" disable_automatic_checks = false auto_install = false [feedback] auto_submit = false ``` A configured Outlook Web account then looks like: ```toml version = 11 default_account = "work" [accounts.work] id = "acc_0123456789abcdef0123456789abcdef" address = "reader@example.invalid" [accounts.work.mail] provider = "microsoft-owa" [accounts.work.mail.outlook_web] origin = "https://outlook.cloud.microsoft" [accounts.work.calendar] provider = "microsoft-owa" [accounts.work.calendar.outlook_web] origin = "https://outlook.cloud.microsoft" [policy] mode = "guarded" preview_sensitive_reads = false preview_reversible_writes = false max_recipients = 20 max_attendees = 50 [browser] login_timeout = "5m0s" [updates] channel = "stable" disable_automatic_checks = false auto_install = false [feedback] auto_submit = false ``` Unknown fields, mismatched tagged-union payloads, unsupported providers, credential-bearing URLs, non-TLS remote endpoints, duplicate account IDs, invalid aliases, and out-of-range policy values are rejected. Do not copy example IDs into multiple accounts. Let `corr account add` generate them. ## Per-service routes Each account may have mail, calendar, tasks, messages, or any explicit combination. Supported route payloads are: | Service | Provider | Nested table | | --- | --- | --- | | mail | `microsoft-owa` | `mail.outlook_web` | | mail | `google` | `mail.google` | | mail | `microsoft-graph` | `mail.microsoft_graph` | | mail | `jmap` | `mail.jmap` | | mail | `imap-smtp` | `mail.imap_smtp` | | calendar | `microsoft-owa` | `calendar.outlook_web` | | calendar | `google` | `calendar.google` | | calendar | `microsoft-graph` | `calendar.microsoft_graph` | | calendar | `caldav` | `calendar.caldav` | | tasks | `microsoft-graph` | `tasks.microsoft_graph` and `tasks.microsoft_graph.oauth` | | tasks | `todoist` | `tasks.todoist` and `tasks.todoist.oauth` | | tasks | `caldav` | `tasks.caldav` | | tasks | `google-tasks` | `tasks.google_tasks` and `tasks.google_tasks.oauth`; user-owned Desktop OAuth | | tasks | `ticktick` | `tasks.ticktick` and `tasks.ticktick.oauth` | | tasks | `microsoft-web-tasks`, `apple-reminders`, `anydo-mcp`, `things`, `omnifocus` | provider only; adapter unavailable | | messages | `microsoft-teams` | exactly one of `messages.teams_graph` or `messages.teams_web`; v0.9 release-gated | | messages | `slack` | `messages.slack`; v0.9 release-gated | | messages | `mattermost` | `messages.mattermost`; v0.9 release-gated | The payload must match the provider exactly. A Google mail-and-calendar route may share one Desktop client, client-credential handle, and grant, and pins the Gmail and Calendar API base. Distinct clients require distinct reviewed handles and consent. Legacy schema-v3 through v10 Google routes fail closed because migration cannot manufacture the new external credential consent. Graph mail and calendar may share one identical API route. An independent IMAP/SMTP mail route can be paired with a CalDAV calendar route. Messaging configuration binds one stable workspace to one explicitly selected transport. Teams Graph may reuse an identical Microsoft public-client grant within the same account; Teams Web always uses the account's dedicated visible browser profile. Slack and Mattermost store only a consented keyring/helper reference to an externally installed authorization. Their token value, a browser cookie, and an authorization header are not representable in this schema. Messaging credential handles cannot be shared across accounts. ```toml [accounts.teams.messages] provider = "microsoft-teams" [accounts.teams.messages.teams_graph] workspace_id = "tenant-synthetic" read_only = true [accounts.teams.messages.teams_graph.oauth] api_base = "https://graph.microsoft.com/v1.0" microsoft_cloud = "global" client_id = "synthetic-public-client" redirect_uri = "http://127.0.0.1:0/callback" [accounts.teams.messages.teams_graph.oauth.authorization] backend = "os-keyring" key = "messages-teams-graph" consent = true ``` The alternative Teams Web payload fixes the provider-owned origin and contains no credential reference: ```toml [accounts.teams.messages] provider = "microsoft-teams" [accounts.teams.messages.teams_web] workspace_id = "tenant-synthetic" read_only = true [accounts.teams.messages.teams_web.web] origin = "https://teams.microsoft.com" ``` Slack pins either its commercial or government API base. Mattermost accepts one exact credential-free HTTPS DNS origin. Before applying authorization, its runtime requires a bounded all-public DNS answer set and pins those addresses for the transport lifetime. It uses normal TLS verification and rejects IP literals, proxies, redirects, mixed/private/special answers, compressed responses, and bounded-body violations. ```toml [accounts.slack.messages] provider = "slack" [accounts.slack.messages.slack] api_base = "https://slack.com/api" workspace_id = "T-SYNTHETIC" read_only = true [accounts.slack.messages.slack.authorization] backend = "os-keyring" key = "messages-slack" consent = true [accounts.mattermost.messages] provider = "mattermost" [accounts.mattermost.messages.mattermost] origin = "https://chat.example.invalid" workspace_id = "team-synthetic" read_only = true [accounts.mattermost.messages.mattermost.authorization] backend = "os-keyring" key = "messages-mattermost" consent = true ``` These are schema examples, not activation instructions. Until the v0.9 manifest records both Teams routes at parity, all provider live observations, surface contracts, documentation, and a clean final security review, every messaging route stops before any external effect. The task route remains closed and secret-free. An implemented Microsoft To Do task-only route looks like: ```toml [accounts.tasks] id = "acc_0123456789abcdef0123456789abcdef" address = "reader@example.invalid" [accounts.tasks.tasks] provider = "microsoft-graph" [accounts.tasks.tasks.microsoft_graph] read_only = true [accounts.tasks.tasks.microsoft_graph.oauth] api_base = "https://graph.microsoft.us/v1.0" microsoft_cloud = "gcc-high" client_id = "synthetic-public-client" redirect_uri = "http://127.0.0.1:0/callback" [accounts.tasks.tasks.microsoft_graph.oauth.authorization] backend = "os-keyring" key = "tasks-graph" consent = true ``` `address` is required for a Microsoft Graph task route and is checked against the delegated `/me` identity on every login. Setup still performs no provider discovery. `read_only = true` selects `Tasks.Read`; omission selects `Tasks.ReadWrite`. `microsoft_cloud` is `global`, `gcc-high`, or `dod`, and the exact API base must match that choice. `china` is a recognized deployment but To Do configuration is rejected because the API is unavailable there. The authority is derived from this closed pair and is not configurable. Todoist uses a separate closed public-client route: ```toml [accounts.todoist] id = "acc_0000000000000000000000000000000a" address = "reader@example.invalid" [accounts.todoist.tasks] provider = "todoist" [accounts.todoist.tasks.todoist] read_only = true [accounts.todoist.tasks.todoist.oauth] api_base = "https://api.todoist.com/api/v1" client_id = "synthetic-public-client" redirect_uri = "http://127.0.0.1:53684/callback" [accounts.todoist.tasks.todoist.oauth.authorization] backend = "os-keyring" key = "tasks-todoist" consent = true ``` Its address is checked against the delegated Todoist identity at login. `read_only = true` selects `data:read`; omission selects `data:read_write,data:delete`. The API base is fixed. Configuration has no client-secret or personal-token field. The loopback port is also fixed and must exactly match the redirect registered for that public client; Todoist routes do not use the ephemeral `:0` convention. TickTick uses a separate confidential-client route: ```toml [accounts.ticktick] id = "acc_0000000000000000000000000000000d" [accounts.ticktick.tasks] provider = "ticktick" [accounts.ticktick.tasks.ticktick] read_only = true [accounts.ticktick.tasks.ticktick.oauth] api_base = "https://api.ticktick.com" client_id = "synthetic-confidential-client" redirect_uri = "http://127.0.0.1:53685/callback" [accounts.ticktick.tasks.ticktick.oauth.authorization] backend = "os-keyring" key = "tasks-ticktick-grant" consent = true [accounts.ticktick.tasks.ticktick.oauth.client_secret] backend = "os-keyring" key = "tasks-ticktick-client-secret" consent = true ``` The authorization grant and client-secret handles must differ, and each is dedicated to this TickTick binding: neither may be reused by another route or account. Corresync owns and may remove only the grant; the client secret remains in the selected external credential owner. `read_only = true` requests `tasks:read`; omission requests `tasks:write`. TickTick documents no PKCE, refresh grant, or identity endpoint, so the fixed-port loopback must exactly match the registered application, token expiry requires a new interactive login, and the missing remote identity check is visible as a degradation. Personal API tokens are not accepted. CalDAV VTODO uses a distinct standards route and credential consent, even when the same account also has a CalDAV VEVENT calendar: ```toml [accounts.tasks] id = "acc_0000000000000000000000000000000b" [accounts.tasks.tasks] provider = "caldav" [accounts.tasks.tasks.caldav] endpoint = "https://dav.example.invalid/" task_list_path = "/dav/tasks/work/" username = "reader@example.invalid" [accounts.tasks.tasks.caldav.credential] backend = "os-keyring" key = "tasks-caldav" consent = true ``` `task_list_path` is optional; authenticated discovery otherwise selects the first VTODO collection. The address may be omitted. The endpoint is HTTPS-only and credential-free, while the password or app-specific password stays in the selected external credential owner. VEVENT and VTODO discovery, sessions, capabilities, cursors, and writes remain separate. Remaining task providers are unavailable and have no arbitrary options map or credential value. See the [task contract](tasks.md). The current Google Tasks route shape is: ```toml [accounts.google-tasks] id = "acc_0000000000000000000000000000000c" address = "reader@example.invalid" [accounts.google-tasks.tasks] provider = "google-tasks" [accounts.google-tasks.tasks.google_tasks] read_only = true [accounts.google-tasks.tasks.google_tasks.oauth] api_base = "https://tasks.googleapis.com" client_id = "synthetic.apps.googleusercontent.com" redirect_uri = "http://127.0.0.1:0" [accounts.google-tasks.tasks.google_tasks.oauth.authorization] backend = "os-keyring" key = "tasks-google" consent = true [accounts.google-tasks.tasks.google_tasks.oauth.client_secret] backend = "os-keyring" key = "google-desktop-client" consent = true ``` The route requests exactly one task scope and cannot reuse a Gmail/Calendar authorization handle. It may reference the same Desktop client credential, but its grant stays separate. Prefer guided setup so each handle and scope is reviewed before persistence. Prefer the lifecycle command over hand editing: ```console corr account add reader@example.invalid \ --alias tasks \ --task-provider microsoft-graph \ --microsoft-cloud gcc-high \ --task-oauth-client-id synthetic-public-client \ --task-oauth-redirect-uri http://127.0.0.1:0/callback \ --task-authorization-key tasks-graph \ --approve-task-oauth \ --task-read-only corr account add reader@example.invalid \ --alias todoist \ --task-provider todoist \ --task-oauth-client-id synthetic-public-client \ --task-oauth-redirect-uri http://127.0.0.1:53684/callback \ --task-authorization-key tasks-todoist \ --approve-task-oauth \ --task-read-only corr account add \ --alias ticktick \ --task-provider ticktick \ --task-oauth-client-id synthetic-confidential-client \ --task-oauth-redirect-uri http://127.0.0.1:53685/callback \ --task-authorization-key tasks-ticktick-grant \ --task-oauth-secret-key tasks-ticktick-client-secret \ --approve-task-oauth \ --approve-task-oauth-secret \ --task-read-only corr account add \ --alias caldav-tasks \ --task-provider caldav \ --task-caldav-endpoint https://dav.example.invalid/ \ --task-list-path /dav/tasks/work/ \ --task-username reader@example.invalid \ --task-credential-key tasks-caldav \ --approve-task-credential ``` Task OAuth flags may use the shared `--oauth-*` client values, but the separate `--approve-task-oauth` flag is always required. TickTick additionally requires `--approve-task-oauth-secret` for its external client-secret handle. Reusing an exact Graph grant within one account combines only the explicitly selected services. A stored grant missing the newly reviewed scope is replaced through fresh interactive authorization. The legacy omitted cloud value and `global` mean the same deployment. One authorization handle cannot be reused for a different provider, public client, redirect, or Microsoft cloud. Use `corr account add` for these combinations; it validates endpoint discovery, explicit provider selection, required consent bits, and route pairing before saving. ## External credentials `config.toml` can hold only a credential reference: ```toml [accounts.work.mail.imap_smtp.credential] backend = "os-keyring" key = "work-mail" consent = true ``` The OS-keyring service name is `corresync`. Store the actual password or token with your platform's keyring facility under the selected key. Corresync reads it only while constructing the explicitly authenticated adapter, bounds it to 64 KiB, keeps it in memory, and overwrites its owned byte buffer on close. An advanced installation can name one helper: ```toml [credentials] helper = ["/absolute/path/to/credential-helper", "get"] [accounts.work.calendar.caldav.credential] backend = "helper" key = "work-calendar" consent = true ``` An account-add review shows the exact backend/key handles before approval. Corresync rejects a new account that attempts to reuse a handle already owned by another configured account; mail, calendar, and task routes belonging to the same account may intentionally share one handle, except that TickTick's grant and client-secret handles are always dedicated. Existing external credential records remain owned by their keyring or helper and are never copied into this file. Guided iCloud setup uses one handle for its IMAP/SMTP and CalDAV routes by default. The post-add enrollment action invokes the platform credential tool with only service `corresync` and that reviewed handle; the OS tool owns the secure prompt and `corr` receives only its exit status. Advanced direct account configuration may still select distinct mail and calendar handles. The executable is invoked directly without a shell. It receives one bounded JSON line on stdin: ```json {"version":1,"operation":"get","key":"work-calendar"} ``` It must return only the secret and an optional final newline on stdout. Stderr is discarded, output is bounded, and the child environment is reduced to a small platform allowlist. Helper arguments and reference keys are private configuration—not suitable for support reports. Corresync never stores a password, cookie, OAuth access/refresh token, authorization header, or browser canary in TOML. ## User-owned Google OAuth route The Google integration uses a Desktop OAuth client in a Google Cloud project you control. Corresync-managed OAuth remains disabled and has no environment, configuration, CLI, MCP, or automatic-fallback override. Follow the [browser-screen setup guide](google-oauth-setup.md); guided `corr setup` is the recommended route. For an advanced scripted setup, securely import the downloaded installed-client JSON first: ```console corr auth google-client import ~/Downloads/client_secret.json \ --key personal-google-client corr account add reader@example.invalid \ --alias personal \ --mail-provider google \ --calendar-provider google \ --oauth-client-id YOUR_CLIENT_ID.apps.googleusercontent.com \ --oauth-redirect-uri http://127.0.0.1:0 \ --authorization-key personal-google \ --approve-oauth \ --oauth-client-secret-key personal-google-client \ --approve-oauth-client-secret corr auth login --account personal ``` Import is create-only. If that handle already exists, choose a new handle. Use `--replace` only to rotate the same Google client credential intentionally; never replace a handle owned by an IMAP, SMTP, CalDAV, or other integration. The redirect must be an allowed loopback `http://127.0.0.1` URI. Port `0` selects an available ephemeral port for public-client registrations that permit native-app loopback ports. `corr auth login` prints the exact service-derived scope set before a provider page can open. The flow validates state; the generated client credential and OAuth grant use separate OS-keyring handles. The configuration `client_secret` table stores only an external reference, never its value. Do not commit or paste the downloaded value into TOML, a CLI argument, MCP, logs, support output, or screen recordings. The normal system browser owns Google sign-in. Gmail then uses the pinned Gmail API with `gmail.modify`; Calendar uses the pinned Google Calendar API; Google Tasks uses a separate task-only grant and the pinned Tasks API. The Gmail adapter never calls the immediate permanent-delete method. Passwords, app passwords, cookies, and custom Google hosts are not accepted by any route. Microsoft Graph and hybrid accounts can use distinct OAuth providers and grants. Prefix calendar settings with `calendar-` and task settings with `task-`, for example `--calendar-oauth-client-id` and `--task-authorization-key`. A calendar-only account uses `--mail-provider none`; `--calendar-provider none` creates a mail-only account. ## Outlook Web routing `origin` is an exact authorization boundary, not a wildcard. Configure the final HTTPS Outlook host used after normal sign-in, with no path. Do not use an identity-provider URL or a vanity redirect. An optional bare `mailbox` address routes a shared/delegated mailbox that the same signed-in user is already allowed to access. It grants no permission and does not manage delegates or folders. ## Monitoring No monitor table means `off`. Enable one account through the CLI so consent advances only one step and all bounds are validated: ```console corr monitor enable --mode notify \ --notification-field sender \ --approve ``` Modes are `off`, `notify`, `queue`, and `agent`. Configuration may include metadata filters, poll interval, debounce, retention, hourly release limit, quiet hours, notification fields, or an absolute runner executable. Agent mode's remote egress declaration requires `approve_remote = true`, which the CLI writes only after `--approve-remote-egress`. Desktop `notify` is available through `notify-send` on Linux and `osascript` on macOS. Windows setup fails before configuration changes because Corresync does not install a registered AppUserModelID; use `queue` or an explicitly configured local `agent` runner there. Old configs, imports, and account additions always default to off. ## Policy and updates There is no unguarded-write mode. The `guarded` policy controls optional previews for sensitive reads and reversible writes; destructive writes and external sends retain mandatory review. `updates.disable_automatic_checks = true` disables opportunistic public release checks without disabling `corr update` or `corr update check`. `CORRESYNC_NO_UPDATE_CHECK=1` provides a process override. Interactive CLI starts check the cached release status for the configured channel and show one short, installation-specific action when an update is available. `stable` is the default. A direct installation can follow fully verified prereleases with: ```console corr config set updates.channel preview ``` Return to the stable channel with `corr config set updates.channel stable`. Switching channels never causes a downgrade. Preview releases are not placed in Homebrew, Scoop, WinGet, APT, DNF, or APK catalogs; a package-managed binary can report a preview but will not mix ownership by installing it. Opt in to verified automatic installation for a standalone/direct binary with: ```console corr config set updates.auto_install true ``` or set `updates.auto_install = true` in TOML. Corresync never runs Homebrew, Scoop, WinGet, or a system package manager: managed installations still show their exact owner command. Automatic installation is default-off and never runs on MCP, daemon, completion, feedback, JSON, piped, or non-interactive paths. Configuration commands are also excluded so consent can always be revoked before another update attempt. `disable_automatic_checks` and `auto_install` cannot both be true. ## Optional public error feedback Automatic issue submission is off by default. It is not telemetry and does not upload a crash dump. A user who deliberately wants lower-friction preview feedback can first sign the external GitHub CLI into `github.com`, then enable: ```console gh auth login --hostname github.com corr config set feedback.auto_submit true ``` The equivalent TOML is `feedback.auto_submit = true`; `corr settings` presents the public destination and complete included/excluded categories before consent. Corresync does not accept or store a GitHub token. Eligible interactive CLI failures create a public Issue through `gh` from a closed, content-free allowlist. MCP, machine-output, configuration-management, and non-interactive commands never submit. Disable future attempts with: ```console corr config set feedback.auto_submit false ``` MCP can see that the choice is enabled but cannot change it. Previous public issues and GitHub-side records are not deleted when the local setting is turned off. ## Config lifecycle `corr config edit` uses `VISUAL`, `EDITOR`, or the platform default, then saves only if strict validation succeeds. `config set` supports the documented simple scalar keys and writes normalized TOML; use account lifecycle commands for tagged provider routes. The daemon publishes a digest of the exact config it loaded. CLI and MCP verify it before use. A binary/protocol change can replace an authenticated old daemon; a config digest change fails closed. Run: ```console corr daemon stop ``` then retry so the new owner starts with the new policy. Older schema files are migrated without changing stable account identity or moving credentials into configuration. See the [migration guide](migration-v0.7.md) for version-specific details.