-
Notifications
You must be signed in to change notification settings - Fork 0
Configuration
Complete reference for config.toml — the Cloud Drive Sync configuration file.
Not configured here: the HTTP server (web UI + REST API) and the MCP server for AI assistants are enabled by command-line flags or environment variables, not by
config.toml— they are per-invocation deployment choices rather than persistent settings. See HTTP Server, MCP Server and the CLI options.
The configuration file is created automatically on first run with sensible defaults. You do not need to create it manually.
| Platform | Default path |
|---|---|
| Linux | ~/.config/cloud-drive-sync/config.toml |
| macOS | ~/Library/Application Support/cloud-drive-sync/config.toml |
| Windows | %APPDATA%\cloud-drive-sync\config.toml |
To use a different path, pass --config PATH to any command:
cloud-drive-sync --config /etc/cloud-drive-sync/config.toml start --foreground[general]
# Logging verbosity. One of: "debug", "info", "warning", "error"
log_level = "info"
[sync]
# How often (in seconds) to poll for remote changes
poll_interval = 30
# What to do when a file is modified on both sides simultaneously.
# "keep_both" — rename the conflicting version and keep both files (default)
# "newest_wins" — keep whichever file has the most recent modification time
# "ask_user" — hold the conflict for manual resolution via UI or CLI
# "local_wins" — always keep the local version; discard the remote change
# "remote_wins" — always keep the remote version; discard the local change
conflict_strategy = "keep_both"
# Maximum number of simultaneous upload/download operations
max_concurrent_transfers = 4
# Seconds to wait after a local file change is detected before syncing.
# Coalesces rapid edits (e.g. autosaves) into a single sync action.
debounce_delay = 1.0
# Export Google Docs/Sheets/Slides to Office formats (.docx/.xlsx/.pptx)
# when downloading. Set to false to download as .gdoc stubs instead.
convert_google_docs = true
# Desktop notification settings
notify_sync_complete = true
notify_conflicts = true
notify_errors = true
# Bandwidth throttling in kilobytes per second. 0 = unlimited.
max_upload_kbps = 0
max_download_kbps = 0
# ── Sync Pair 1 ──────────────────────────────────────────────────────────────
[[sync.pairs]]
# Absolute path to the local directory to sync
local_path = "/home/alice/Documents"
# Remote folder identifier.
# Google Drive: folder ID from the URL, or "root" for My Drive top level
# Nextcloud: path relative to the WebDAV root, e.g. "/Documents"
# Dropbox: path, e.g. "/Documents"
# OneDrive: path, e.g. "/Documents"
# Box: folder ID from the URL
remote_folder_id = "root"
# Email address of the account to use for this pair
account_id = "alice@gmail.com"
# Provider for this pair
provider = "gdrive"
enabled = true
sync_mode = "two_way" # "two_way", "upload_only", or "download_only"
ignore_hidden = true # Exclude files/directories starting with "."
# Glob patterns to exclude (in addition to built-in defaults)
ignore_patterns = [
"*.tmp",
"*.log",
"~$*", # Office temp files
"node_modules/",
]
# Per-pair conflict strategy — overrides the global setting for this pair only.
# Omit this key to inherit the global conflict_strategy.
# conflict_strategy = "newest_wins"
# ── Sync Pair 2 ──────────────────────────────────────────────────────────────
[[sync.pairs]]
local_path = "/home/alice/Pictures"
remote_folder_id = "0B3xRemoteFolderIdHere"
account_id = "alice@gmail.com"
provider = "gdrive"
enabled = true
sync_mode = "upload_only"
ignore_hidden = true
ignore_patterns = ["*.raw", "Lightroom/"]
# ── Sync Pair 3 (Nextcloud) ───────────────────────────────────────────────────
[[sync.pairs]]
local_path = "/home/alice/Work"
remote_folder_id = "/Work"
account_id = "alice@company.com"
provider = "nextcloud"
enabled = true
sync_mode = "two_way"
ignore_hidden = true
# ── Accounts ──────────────────────────────────────────────────────────────────
[[accounts]]
email = "alice@gmail.com"
display_name = "Alice (Google)"
provider = "gdrive"
[[accounts]]
email = "alice@company.com"
display_name = "Alice (Work Nextcloud)"
provider = "nextcloud"
server_url = "https://cloud.company.com"| Key | Type | Default | Description |
|---|---|---|---|
log_level |
string | "info" |
Logging verbosity: "debug", "info", "warning", "error"
|
| Key | Type | Default | Description |
|---|---|---|---|
token |
string | generated on a fresh install | Shared token required on /api/* and the web UI. Empty or absent means no authentication. --http-token / CDS_HTTP_TOKEN override it — see Authentication
|
trust_proxy |
bool | false |
Believe X-Forwarded-Proto and X-Forwarded-For from whatever is in front of the daemon. Turn this on only behind a reverse proxy you control: it is what lets the session cookie be marked Secure for a TLS deployment the daemon itself only ever sees as HTTP. Off by default because otherwise anyone who can reach the port can assert those headers |
A new install generates the token on first start and prints it to the log; an existing install is left alone, so an upgrade cannot lock you out. Delete the value to disable authentication again.
The web UI account is not configured here. It lives in the database (
state.db), because a password change is a row update rather than a rewrite of this whole file. Manage it withcloud-drive-sync user set/show/clear— see Signing in to the web UI.
Global sync behavior. All settings here can be overridden per pair where noted.
| Key | Type | Default | Description |
|---|---|---|---|
poll_interval |
integer | 30 |
Seconds between remote change polls |
stopped |
boolean | false |
Emergency stop state; set by the UI/CLI, not meant for hand-editing — see Emergency Stop |
max_deletions_per_sync |
integer | 100 |
Refuse deletions exceeding this many files in one direction within deletion_window_seconds, until confirmed. 0 disables the guard — see Delete Protection
|
deletion_window_seconds |
integer | 60 |
Sliding window the deletion cap is counted over, across sync passes. 0 = per pass only |
conflict_strategy |
string | "keep_both" |
Default conflict resolution strategy (see values below) |
max_concurrent_transfers |
integer | 4 |
Parallel upload/download limit — see Large File Uploads for how big files are chunked |
debounce_delay |
float | 1.0 |
Seconds to wait after a local change before syncing |
convert_google_docs |
boolean | true |
Export Google Docs/Sheets/Slides to Office formats on download |
notify_sync_complete |
boolean | true |
Desktop notification when a sync cycle completes |
notify_conflicts |
boolean | true |
Desktop notification when a conflict is detected |
notify_errors |
boolean | true |
Desktop notification on sync errors |
max_upload_kbps |
integer | 0 |
Upload bandwidth cap in KB/s; 0 = unlimited |
max_download_kbps |
integer | 0 |
Download bandwidth cap in KB/s; 0 = unlimited |
| Value | Behavior |
|---|---|
"keep_both" |
Rename the conflicting version (e.g. file (conflict 2024-01-15).txt) and keep both. Default. |
"newest_wins" |
Keep whichever version has the most recent modification time; discard the other. |
"ask_user" |
Pause the conflict for manual resolution. Visible in the UI under Conflicts and via cloud-drive-sync conflicts. |
"local_wins" |
Always keep the local copy; discard the remote change. |
"remote_wins" |
Always keep the remote copy; discard the local change. |
One [[sync.pairs]] section per sync pair. Multiple pairs are supported.
| Key | Type | Default | Description |
|---|---|---|---|
local_path |
string | (required) | Absolute path to the local directory |
remote_folder_id |
string | "root" |
Remote folder — Drive folder ID, "root" for Drive root, or path for Nextcloud/Dropbox/OneDrive |
account_id |
string | (required) | Email of the account to use |
provider |
string | (required) |
"gdrive", "nextcloud", "dropbox", "onedrive", or "box"
|
enabled |
boolean | true |
Whether this pair is active |
sync_mode |
string | "two_way" |
"two_way", "upload_only", or "download_only"
|
ignore_hidden |
boolean | true |
Exclude files and directories whose names start with .
|
ignore_patterns |
list of strings | [] |
Glob patterns for files to exclude (see Selective Sync below) |
conflict_strategy |
string | (inherits global) | Per-pair conflict strategy; overrides [sync].conflict_strategy if set |
max_deletions_per_sync |
integer | (inherits global) | Per-pair delete cap; 0 disables delete protection for this pair |
deletion_window_seconds |
integer | (inherits global) | Per-pair window for the delete cap |
force_polling |
boolean | false |
Nextcloud only: skip notify_push and always walk the tree — see Nextcloud Change Detection
|
uid |
string | (assigned automatically) | Stable identifier for this pair. Written by the daemon; you do not need to set it. See below. |
Assigned automatically. You never need to write it, and editing it by hand is a bad idea — see below.
Internally a pair is identified by its position in this file: pair N is pair_N
in the engine and in the state database. That works because removing a pair renumbers
the stored rows to match, but the number is not stable across edits. uid is a stable
identity that does not move.
Behaviour worth knowing:
-
New pairs get a random
uidwhen they are created. -
Pairs that predate the field get one derived from
provider,account_id,local_pathandremote_folder_idthe first time the config is loaded. It is written out the next time any setting is saved. - Once written, the
uidis fixed. Changinglocal_pathafterwards does not change it — the derivation is a one-time bridge, not an ongoing function of those fields. - Deleting the
uidline causes a new one to be derived on the next load. If the pair has since been moved, the derived value will differ from the original, so anything keyed on the old value will see it as a different pair.
| Value | Description |
|---|---|
"two_way" |
Changes on either side are synced to the other. |
"upload_only" |
Local changes are uploaded; remote changes are ignored. |
"download_only" |
Remote changes are downloaded; local changes are ignored. |
One [[accounts]] section per cloud account. Accounts are normally managed via cloud-drive-sync account add and written to this section automatically — you rarely need to edit this by hand.
| Key | Type | Description |
|---|---|---|
email |
string | Account identifier (email address) |
display_name |
string | Human-readable label shown in the UI |
provider |
string |
"gdrive", "nextcloud", "dropbox", "onedrive", or "box"
|
server_url |
string | Nextcloud only — base URL of the Nextcloud server, e.g. https://cloud.example.com
|
Where credentials live. Cloud credentials — OAuth tokens and Nextcloud app passwords — are not in config.toml. They are encrypted at rest in separate files, owner-readable only (0600); see Authentication for the layout. No system keychain is involved, on any platform.
The one secret that is in config.toml is the web UI access token, under [http] token. That is why the file is written 0600 — it is the credential for /api/* and the whole UI. If you have copied your config somewhere less protected, treat that token as exposed and replace it.
Outbound HTTP callbacks. When something happens — a sync finishes, a conflict appears, a mass deletion is refused — the daemon posts a JSON event to an endpoint you choose.
Configurable at two levels, which merge: global ([webhooks]) and per pair
([sync.pairs.webhooks]). A pair can override a field of an inherited callback, switch
one off, or define one that exists nowhere else.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Master switch for this level. false at a level disables every target at that level and below; a pair can set true to opt back in |
allow_private_addresses |
boolean | true |
Global only. Set false on a shared deployment to refuse private and link-local endpoints. Not overridable per pair |
Applied to every target that does not set the field itself. A target's own value always wins.
| Key | Type | Default | Description |
|---|---|---|---|
timeout_seconds |
integer | 15 |
Total request timeout. The connect timeout is fixed at 5s |
max_attempts |
integer | 5 |
Attempts per event, including the first |
verify_tls |
boolean | true |
Set false only for a self-signed endpoint on your own network |
include_paths |
boolean | true |
When false, file paths are replaced by SHA-256 hashes and local_path is omitted. See Paths are personal data
|
max_files_per_event |
integer | 100 |
Cap on path samples per event. 0 omits the lists entirely |
One block per callback.
| Key | Type | Default | Description |
|---|---|---|---|
name |
string | (required) | The merge key across levels. A pair overriding a callback names the same value |
define |
boolean | false |
Required when introducing a new name, rejected for an existing one. See below |
url |
string | (required) | Where to POST |
events |
list of strings | (required) | Event names, or one glob segment (conflict.*, or * for everything). Replaces the inherited list |
events_add |
list of strings | [] |
Add to the inherited list. Applied at this level only, never inherited further down |
events_remove |
list of strings | [] |
Remove from the inherited list. Applied after events_add, at this level only |
headers |
table | {} |
Extra request headers, merged per key with the inherited ones |
headers_remove |
list of strings | [] |
Remove inherited header keys. This level only |
enabled |
boolean | (inherits) |
false switches this target off for this level and below |
timeout_seconds |
integer | (inherits) | Overrides [webhooks.defaults]
|
max_attempts |
integer | (inherits) | Overrides [webhooks.defaults]
|
verify_tls |
boolean | (inherits) | Overrides [webhooks.defaults]
|
include_paths |
boolean | (inherits) | Overrides [webhooks.defaults]
|
max_files_per_event |
integer | (inherits) | Overrides [webhooks.defaults]
|
Omit any key to inherit it from the level above. Omitting is not the same as setting a default: an omitted key keeps tracking the level above when you later change it there.
name is how levels are merged, so without a declaration of intent the daemon cannot
tell "override the inherited callback" from "define my own". That ambiguity fails
silently in the worse direction: if you write a new callback whose name happens to
match a disabled one above, it inherits enabled = false and never fires, with a
perfectly valid URL and event list so nothing else looks wrong.
So: define = true when the name is new, and omit it when overriding. Getting it
the wrong way round is reported at startup and by cloud-drive-sync webhook list,
naming both levels.
How to authenticate. mode is required whenever this block is present — the block
is replaced wholesale rather than merged, so a pair overriding just a token would
otherwise lose the inherited mode and silently downgrade to unauthenticated.
| Mode | Required keys | Header sent |
|---|---|---|
none |
— | none |
basic |
username, and password or password_env
|
Authorization: Basic base64(user:pass) |
bearer |
token or token_env
|
Authorization: Bearer <token> |
custom |
header, and value or value_env
|
<header>: <value> |
custom accepts header = "Authorization", which covers schemes that are not modelled
(Authorization: Token abc).
Every secret has an _env twin — token_env, password_env, value_env — naming an
environment variable to read at request time. Prefer it. The value never touches the
config file, so a config backup carries no credential, and it is how a container or
systemd unit is meant to supply secrets. A literal wins over an _env reference if both
are set.
Optional HMAC signature over the request body. Composes with any auth mode, including
none — which is the best combination if your receiver can verify signatures, because
then no long-lived credential is transmitted at all.
| Key | Type | Default | Description |
|---|---|---|---|
secret / secret_env
|
string | (required) | The shared signing secret |
algorithm |
string | "sha256" |
"sha256" or "sha512"
|
header |
string | "X-CDS-Signature" |
Header carrying <algorithm>=<hex digest>
|
timestamp_header |
string | "X-CDS-Timestamp" |
Header carrying the Unix timestamp |
The digest covers "<timestamp>.<body>", so the timestamp is inside the signed
material and a captured body cannot be replayed later under a fresh one. Verify against
the raw bytes you received, not a re-serialised copy.
| Event | When |
|---|---|
sync.completed |
A sync pass finished with something to report |
sync.failed |
A sync pass raised |
conflict.detected |
Both sides changed the same file |
deletion.blocked |
Delete protection refused a batch |
activity.stopped / activity.resumed
|
Emergency stop |
transfer.progress |
Per-chunk transfer progress. Very high volume; opt in deliberately |
webhook.test |
Sent by cloud-drive-sync webhook test
|
deletion.blocked is the one worth wiring to something you actually watch. It fires
when the daemon has refused to delete files and is waiting for a human, and its payload
carries the full breach — including ratio, which is what distinguishes a wiped source
from an ordinary cleanup.
A webhook sends your filenames to another system. If that system is outside your
control, set include_paths = false: paths become SHA-256 hashes and local_path is
omitted from the payload, which is enough to count and correlate events but not to read
them.
[webhooks]
enabled = true
[webhooks.defaults]
timeout_seconds = 15
max_attempts = 5
# A callback for everything, authenticated with a token from the environment.
[[webhooks.targets]]
define = true
name = "ops-bus"
url = "https://ops.example.com/hooks/cds"
events = ["sync.completed", "sync.failed", "deletion.blocked"]
auth = { mode = "bearer", token_env = "CDS_OPS_TOKEN" }
# A Home Assistant automation on the LAN; no credential needed.
[[webhooks.targets]]
define = true
name = "home-assistant"
url = "http://ha.lan:8123/api/webhook/cds"
events = ["sync.completed", "conflict.detected"]
auth = { mode = "none" }
[[sync.pairs]]
uid = "3f7a1c68-2d4e-4f0b-9a11-8c5e6b0d2a94"
local_path = "/home/me/Documents"
account_id = "me@example.com"
provider = "gdrive"
[sync.pairs.webhooks]
# Only tell ops about refused deletions for this folder.
[[sync.pairs.webhooks.targets]]
name = "ops-bus"
events = ["deletion.blocked"]
# And do not notify Home Assistant about this folder at all.
[[sync.pairs.webhooks.targets]]
name = "home-assistant"
enabled = false
# A callback only this folder has. `define` because the name is new.
[[sync.pairs.webhooks.targets]]
define = true
name = "photo-indexer"
url = "http://nas.lan:9000/reindex"
events = ["sync.completed"]
auth = { mode = "custom", header = "X-API-Key", value_env = "NAS_KEY" }Run cloud-drive-sync webhook list --scope pair:<uid> to see what that actually
resolves to, rather than working it out by hand.
There are two ways to exclude files from syncing.
Add glob patterns to the ignore_patterns list of a sync pair:
[[sync.pairs]]
local_path = "/home/alice/Documents"
# ...
ignore_patterns = [
"*.tmp",
"*.log",
"~$*",
"node_modules/",
"build/",
".venv/",
]Patterns are matched against file names (not full paths). A trailing / matches directories only.
Place a .cloud-drive-sync-ignore file in the root of any synced folder. It uses the same syntax as .gitignore:
# Ignore all log files
*.log
# Ignore temp files
*.tmp
~$*
# Ignore build output directories
build/
dist/
.cache/
# Ignore Python virtual environments
.venv/
__pycache__/
*.pyc
# Ignore a specific file
secrets.txt
# Ignore everything in a subdirectory except one file
private/*
!private/README.md
The .cloud-drive-sync-ignore file itself is never synced.
The .cloud-drive-sync-ignore file in a sync folder's root applies only to that folder. Subdirectories do not have their own ignore files — patterns from the root file apply recursively to the entire sync tree.
The following are excluded from every sync pair regardless of configuration:
| Pattern | Reason |
|---|---|
.git/ |
Git repository metadata — syncing this causes repository corruption |
__pycache__/ |
Python bytecode cache — platform-specific, regenerated automatically |
.DS_Store |
macOS Finder metadata |
Thumbs.db |
Windows Explorer thumbnail cache |
.Trash-*/ |
Linux trash directories |
These cannot be overridden — they are hardcoded as safety defaults to prevent common mistakes.
Cloud Drive Sync
Getting Started
Reference
Project