Skip to content

CLI reference

CloneDB has seven subcommands. Four build and maintain the config (init, generate-config, sync-config, config), one checks it (validate), one does the work (run), and one diagnoses it (explain).

clonedb [GLOBAL OPTIONS] <COMMAND> [COMMAND OPTIONS]

The config lifecycle is: create a config (init or generate-config) → check it (validate) → run it (run) → reconcile it after schema changes (sync-config).

These are accepted before and after the subcommand (they’re global), so clonedb -v run and clonedb run -v are equivalent.

FlagDefaultDescription
--config <FILE>clonedb.tomlPath to the config file every command reads/writes.
--env-file <FILE>offLoad this .env into the environment before resolving credentials (overrides the config’s env_file).
-v, --verboseoffIncrease log verbosity. Repeatable: -v (info), -vv (debug), -vvv (trace).
--jsonoffEmit machine-readable JSON instead of human-formatted output.
-q, --quietoffSuppress all non-error output. Conflicts with --verbose.

Interactive wizard. Walks you through defining named connections (and which are the default source/target), then connects to introspect and lets you choose schemas, tables, and filters before writing the config. For each connection you can paste a connection string (parsed into host/port/db/user/sslmode — any password is stripped, since the provider supplies the secret) or enter the fields one at a time. Multi-selects use Enter to toggle each item and a ✓ Submit entry to finish; single-choice menus pick on Enter. Use this for a first config; use generate-config for CI/scripting.

Every prompt’s help line shows that Esc cancels the wizard. List prompts open with the cursor on the first real option and offer a ← Back entry (the first prompt has none) — choose it to return to the previous prompt and revise an earlier answer without losing what you’ve already entered. The final “Write config?” step is a 3-way Yes / No / ← Back. See Back-navigation & Esc for the shared rules.

When you pick the env or dotenv provider, the wizard asks for the .env file once (creating it if missing), then for each variable checks whether it’s already defined and, if not, prompts for its value (the same paste-or-fields URL flow) and writes it in. The chosen file is recorded as the config’s env_file.

Terminal window
clonedb init
clonedb init --source "postgres://reader@host/app" -o clonedb.toml
FlagDescription
--source <URL>Source connection URL to introspect. Overrides $CLONEDB_SOURCE_URL.
-o, --output <FILE>Where to write the config. Defaults to --config.
--forceOverwrite an existing config file instead of refusing.

Non-interactive config generation from introspection — the scriptable counterpart to init.

Terminal window
clonedb generate-config --schemas public,billing --source "$SOURCE_DATABASE_URL"
clonedb generate-config --tables public.users,public.orders -o subset.toml
FlagDefaultDescription
--schemas <SPEC>allSchemas to include: all, a single schema, or a comma-separated list.
--tables <LIST>allRestrict to these schema-qualified tables (comma-separated).
--exclude-tables <LIST>noneExclude these schema-qualified tables (comma-separated).
--all-tablesoffEmit an explicit empty [tables."schema.table"] block for every table (verbose enumeration). Default: minimal — tables are implicit, only blocks carrying config are written, and the rest are listed in the managed “available tables” comment.
--source <SRC>$CLONEDB_SOURCE_URLSource to introspect — a URL, or (when regenerating an existing config) a connection name / default_source.
--source-name <NAME>sourceName of the source [connections.<name>] block to emit + set as default_source.
--target-name <NAME>targetName of the target connection block to emit + set as default_target.
-o, --output <FILE>--configWhere to write the generated config.
--forceoffOverwrite an existing config file. Without it, generate-config refuses to clobber an existing config (which would discard your edits and filters); choose another -o path, or use sync-config for an edit-preserving update.

Regenerate the config against an evolved schema, preserving your edits and comments. Because tables are included by default, new tables need no blocks — the “available tables” comment is refreshed instead; a configured table that was removed is flagged (not silently deleted). This is the command that lets a config survive migrations.

Terminal window
clonedb sync-config # preview the diff only (default — writes nothing)
clonedb sync-config --write # apply the merge to the config file
clonedb sync-config --force # clean-regenerate, discarding edits + comments
FlagDescription
--writeApply the merge to the config file. Without it, sync-config only previews the diff.
--forceClean-regenerate from scratch, discarding existing edits and comments.
--source <SRC>A connection name (from [connections.*]) or a URL. Omit it to use default_source → the source connection → $CLONEDB_SOURCE_URL.
-o, --output <FILE>Where to write the synced config. Defaults to --config.

Make a single, validated edit to the config from the command line — without hand-editing TOML and without re-running init/generate-config (which regenerate). Edits go through the same comment- and format-preserving writer sync-config uses, so only the touched key changes, and a line diff of the change is printed.

config edits these areas:

AreaSubcommandEdits
Defaultsconfig defaults {set,unset,show} <field> [value]the [defaults] policy block
Per-tableconfig table <schema.table> {set,unset,show,add,remove} <field> [value]a [tables."schema.table"] block
Schemasconfig schemas {include,exclude} {add,remove} [name…]the [schemas] include/exclude lists
Excludeconfig exclude {add,remove} [table…]the top-level exclude table list
Passthroughconfig passthrough {add,remove} [table…]the top-level passthrough table list
Virtual FKsconfig virtual-fks {infer,add,remove,show}the [virtual_foreign_keys] map (and [[virtual_foreign_key_rules]])

Connections and auth are intentionally out of scope — they carry secrets, so use init.

Terminal window
clonedb config defaults set on_missing_parent error
clonedb config table public.audit_log set exclude true
clonedb config table public.orders add exclude_columns card_number
clonedb config schemas include add billing
clonedb config exclude add public.audit_log public.sessions
clonedb config defaults set max_children_per_parent 2000 --dry-run

Every config run resolves the source and introspects the live schema (nothing caches it), exactly like validate and sync-config. The live schema powers the wizard’s menus and existence-checks every reference before writing: an unknown table, a bad column for exclude_columns/date.column, or a schema that doesn’t exist all fail loudly with the valid options listed. (--help and argument parsing never need a database.)

Run clonedb config with trailing arguments missing and it drops into a guided wizard that resumes at the first slot you left out — pick an area, then an action, then a field (shown with its valid values), then a value (enums/bools as a menu; tables, columns, and schemas from the real introspected names). Every argument you supply skips its prompt, so the command line and the wizard are one path:

InvocationWizard resumes at
clonedb configpick an area → action → field → value
clonedb config tablepick a table → action → field → value
clonedb config table public.usersaction → field → value
clonedb config table public.users setfield → value
clonedb config table public.users set excludevalue
clonedb config table public.users set exclude truenothing — applied directly

A supplied value is validated identically to a chosen one. In a non-interactive environment a partial invocation fails closed, naming the missing slot.

Each prompted rung offers a ← Back entry that returns to the previous prompted rung (area → action → field → value), so you can revise a wrong turn without restarting. Rungs you supplied on the command line are skipped and are never back targets← Back only steps through prompts the wizard actually showed. See Back-navigation & Esc.

init and config share one prompt layer, so navigation and help text are identical in both:

  • Esc cancels the entire wizard at any prompt — every prompt’s help line says so.
  • ← Back is the first entry on navigable list prompts. Choosing it returns to the previous prompt and lets you revise an earlier answer without losing answers you’ve already given. The first prompt shows no ← Back (nothing precedes it).
  • List prompts open with the cursor on the first real option, not on ← Back or ✓ Submit.
  • Free-text and yes/no prompts have no list to host a ← Back row; you back into them by choosing ← Back on the next list prompt. Where a back point matters most — init’s final “Write config?” — the confirm is a 3-way Yes / No / ← Back select instead.
  • List fields (ids, exclude_columns, the [schemas] lists, and the top-level exclude/passthrough lists) are edited element-wise with add/remove (idempotent) — never by restating the whole list. The four set-lists (schemas include/exclude, exclude, passthrough) share one UX: pass names to script it, or omit them to pick from a multiselect (see Exclude / passthrough lists).
  • Seed sources are where/date, ids, or sql — a table can be seeded from only one of the three groups. where and date compose (both set → AND-ed); setting a source from a different group while one is present is an error non-interactively (unset the other first); the wizard offers to clear it. (limit isn’t a source — it combines freely.)
  • The date restriction is three scalar sub-fields — date.column, date.start, date.end (e.g. clonedb config table public.events set date.column created_at).
  • show reports only the explicitly set values, not the effective post-inheritance value — that’s explain’s job.

config exclude and config passthrough manage the two top-level arrays of schema-qualified table names — the bulk list form of the per-table exclude = true / passthrough = true blocks, additive to them. Each is an add/remove set-list with the same UX as config schemas:

ActionWhat it does
config exclude add [table…]Add tables to the exclude list. With names: each is existence-checked against the live schema first. With no names: a multiselect of live tables not yet listed.
config exclude remove [table…]Drop tables from the exclude list. With no names: a multiselect of the current members (a removed table needn’t still exist in the source).
config passthrough add|remove [table…]The same, for the passthrough list.
Terminal window
clonedb config exclude add public.audit_log public.sessions # script several at once
clonedb config exclude remove public.sessions # idempotent
clonedb config passthrough add billing.countries
clonedb config exclude # bare → action, then a checkbox menu

The edit is comment- and format-preserving and idempotent (re-adding a listed table is a no-op); add writes the array at the top of the file (TOML requires top-level keys above the first [section]). The same multiselect was retrofit onto config schemas — running config schemas include (or exclude) with no name now opens a checkbox menu of the candidate schemas, so all four set-lists behave identically. exclude wins over passthrough when a table is in both; validate flags that and other redundancies.

Many schemas (Rails, Django, anything app-enforced) declare almost no database-level foreign keys, so clonedb has nothing to traverse. config virtual-fks declares those relationships — one at a time, or in bulk by naming convention:

ActionWhat it does
config virtual-fks infer [--apply|--dry-run]Scan every <thing>_id column and propose <thing>_id → <things>.id, writing the terse map.
config virtual-fks add <child.schema.table.col> <parent.schema.table[.col]> [--when <pred>]Add one. Parent column defaults to the parent’s PK. Auto-routes: plain → terse map; --when or an expression child column → a rule.
config virtual-fks remove <child.schema.table.col>Drop the entry matching that child column (from either form).
config virtual-fks showList the configured virtual FKs (both forms, merged).
Terminal window
clonedb config virtual-fks infer # preview the inferred set
clonedb config virtual-fks infer --apply # …and write all of them (terse map)
clonedb config virtual-fks add public.notes.target_id public.orders
clonedb config virtual-fks add public.comments.commentable_id public.posts \
--when "commentable_type = 'post'" # → a [[virtual_foreign_key_rules]] entry
clonedb config virtual-fks remove public.notes.target_id

infer/add write the slim [virtual_foreign_keys] map for the common single-column case; add --when (or an expression child column) lands a [[virtual_foreign_key_rules]] entry, since the map can’t hold those. show lists both forms merged; remove clears from whichever form holds the child.

The convention. infer matches a <thing>_id column to the table whose singular/plural name is <thing> (via an inflector, so category_id → categories, person_id → people), preferring a table in the child’s own schema and otherwise requiring a single unambiguous match across the included schemas. The target is always that table’s single-column primary key (usually id), and the column types must be compatible. Every inferred entry is a plain single-column FK — exactly the kind the run engine follows.

Skipped — and reported, never silent. infer refuses to guess. It prints a reason for every column it leaves out: already a real foreign key, already covered by an existing virtual FK (so re-running is idempotent), polymorphic (<thing>_id next to a <thing>_type sibling), a composite- or no-PK target, a type mismatch, or an ambiguous / no-matching-table name (public_id, legacy_id, …). Those are yours to add by hand if real.

Inferred 9 virtual foreign key(s) (3 column(s) skipped):
public.comments.author_id -> public.users.id
public.posts.category_id -> public.categories.id
billing.invoices.order_id -> public.orders.id
...
Skipped:
public.comments.commentable_id (polymorphic: sibling column `commentable_type`)
public.order_items.order_id (already a real foreign key)
public.events.legacy_id (no table matches `legacy`)
...

Review before writing. The preview is the default. In an interactive terminal, infer opens a multiselect with every candidate pre-selected — deselect the false positives, submit to write the rest. Non-interactively it prints the set and writes nothing unless you pass --apply (alias --yes); with neither flag and no terminal it fails closed. --dry-run previews the config diff and writes nothing. add/remove existence-check both endpoints against the live schema before writing, and all four actions go through the same comment-preserving writer as the rest of config.

FlagDescription
--source <SRC>A connection name or a URL. Omit it to use default_source → the source connection → $CLONEDB_SOURCE_URL.
--dry-runPrint the line diff the edit would make, without writing the file.

A safe, read-only confidence check, and a prerequisite for every run. It confirms that every schema-qualified reference in the config resolves against the live database:

  • [schemas].include names a schema that exists.
  • every [tables."schema.table"] key is schema-qualified and the table exists.
  • per-table exclude_columns and date.column name real columns.
  • [virtual_foreign_keys] / [[virtual_foreign_key_rules]] and [[relationship_overrides]] endpoints name real tables (SQL-expression virtual-FK columns are intentionally not column-checked).
Terminal window
clonedb validate
clonedb --json validate # stable machine-readable report for CI
FlagDescription
--source <SRC>A connection name or a URL. Omit it to use default_source → the source connection → $CLONEDB_SOURCE_URL.

Perform a subset extraction. By default run is run-once-exit: one subset run, then return. One output flag picks where the subset goes — --dry-run (plan only), --output-sql (a SQL dump), or --to/--target (a live load). The three are mutually exclusive and rejected at parse time; pick one per run.

Terminal window
clonedb run --dry-run # plan only, move nothing
clonedb run --output-sql subset.sql # write a portable SQL dump
clonedb run --target "$DEV_DATABASE_URL" # idempotent upsert into a live db

run reads from a source connection and (optionally) writes to a target one. Either side is a [connections.<name>] name or a raw URL, so you can move data between any two of your named connections.

FlagDescription
--from <SRC>Source connection — a connection name or a raw URL. Overrides default_source.
--to <DST>Target connection — a name or a raw URL. Overrides default_target.
--target <TARGET>Back-compat alias of --to.

When a flag is omitted, each side falls back to its default_source/default_target pointer, then a connection conventionally named source/target, then (source only) $CLONEDB_SOURCE_URL.

Terminal window
clonedb run --from production --to local --dry-run
clonedb run --from staging --to dev --output-sql slice.sql
FlagDescription
--dry-runPrint the plan — a fast per-table planner estimate — without moving data.
--output-sql <FILE>Write the collected subset to a SQL dump file.
--target <TARGET>Load the subset into the target database. Alias of --to (a connection name or URL) — see Source & target.
--truncateBefore loading, TRUNCATE the target tables this run authoritatively defines — the whole (fully-cloned) tables and the seeded/scoped tables plus their children — instead of upserting in place. Incidental upstream FK parents (pulled only as partial subsets for closure) are upserted, not truncated, so unrelated target rows aren’t wiped. Never uses CASCADE. Off by default.
--truncate-cascadeLike --truncate but TRUNCATE … CASCADE — also empties rows in tables outside the clone that FK-reference a truncated table. Use when a plain --truncate errors because an out-of-scope table references a cloned one. More destructive (can wipe unrelated rows). Implies truncation; if combined with --truncate, cascade wins.
--yesSkip the confirmation prompt(s) before writing to a target.

The target load is an idempotent upsert keyed on each table’s primary key (or a table’s upsert_key when it has no PK), so a run is re-runnable. Writing to a target prompts for confirmation first; --yes bypasses it. In a non-interactive environment a target write fails closed unless --yes is passed (the target URL is never printed — it may carry a secret).

A foreign-key cycle — a self-reference (users.manager_id → users) or a mutual A ↔ B — can’t be inserted in any single topological order. clonedb breaks cycles with a portable two-pass that needs no flag and no special privilege: it inserts the cycle’s rows with their into-cycle FK columns NULL, then issues UPDATEs to fill those columns in once every row exists. A plain psql -f dump.sql or a direct --to load just works.

A cyclic table pulled whole streams its back-fill UPDATEs via a second pass (a re-query of (pk, into-cycle columns) emitted in batches) instead of buffering them, so a large self-referencing table — a tree or threaded table — stays within the same bounded memory as any other whole-table stream.

The two-pass can’t NULL a NOT NULL into-cycle column, so such a cycle can’t be broken that way. By default run stops with a clear UnbreakableCycle error that points at the opt-in flag:

FlagDescription
--defer-constraintsLoad a NOT NULL FK cycle by deferring the constraint check instead of the portable two-pass. Off by default.

With --defer-constraints, clonedb inserts the real FK values and defers the check rather than nulling-then-back-filling:

  • Deferrable cycles (portable). When the cycle’s FKs are declared DEFERRABLE, clonedb uses SET CONSTRAINTS ALL DEFERRED so the check happens at COMMIT. For --output-sql the dump is wrapped in BEGIN; SET CONSTRAINTS ALL DEFERRED; … COMMIT; — the file must be applied atomically (as one transaction) against a target whose matching constraints are themselves DEFERRABLE.
  • Non-deferrable cycles (--to only, privileged). On a direct --to load, a cycle whose FKs are not deferrable additionally falls back to session_replication_role = replica, which bypasses FK enforcement for the load transaction. This needs superuser / replication privilege; if the session can’t set it, clonedb fails with a clear error rather than silently loading unchecked data. Because it requires privilege, this path is never baked into a portable --output-sql dump — a non-deferrable cycle there is rejected.

--defer-constraints is strictly opt-in: without it, behavior is unchanged (the portable two-pass, and the UnbreakableCycle error on a NOT NULL cycle). Use explain to see which tables sit in a cycle before a run.

FlagDescription
--scope <NAME>Restrict the run to a named [scopes.<name>]bounds the pull: the scope’s tables become the run’s seeds (the config’s own seeds and ambient cascade are neutralized), and only those tables plus their FK parents are pulled — children only where a scope override re-enables a cascade. Everything else is skipped, not cloned whole. Repeatable; the union is used.
--scopedBound the run to its seeds — pull only the seeded tables plus the FK parents they require (children only where a cascade is opted in), skipping the unreachable complement instead of cloning it whole. Works for any seeded run: with --seed, those tables act as the scope and the config’s other seeds and ambient follow_children cascade are neutralized (like a named --scope); without --seed, the config’s own seeds drive the run unchanged, just bounded. Errors only if there’s no seed anywhere (no --seed, no --scope, no config seed) — a bounded run over an empty seed set collects nothing. Implied by --scope.
--seed <TABLE>Ad-hoc seed table (repeatable), overlaying the config for one run.
--where <PREDICATE>Restrict all the --seed tables with one shared SQL predicate (e.g. id = 123).
--filtered-seed <TABLE> <PREDICATE>Seed one table with its own predicate (repeatable, two values). Unlike the single global --where, each occurrence carries a different filter, so different seeds get different conditions.
--ids <LIST>Restrict the --seed tables to these primary-key ids (comma-separated).
--sql <QUERY>Seed the --seed tables from this raw SELECT.
--follow-childrenAnchor a downstream cascade from the --seed / --filtered-seed tables.

--where, --ids, and --sql are mutually exclusive restrictions on the ad-hoc --seed set. --filtered-seed is the per-table alternative to the shared --where: its <PREDICATE> is raw SQL (write AND/OR/parentheses as needed — SQL precedence applies, AND binds tighter than OR), and naming one table in more than one seed flag is an error (combine its conditions into a single predicate). Use these for one-off pulls without editing the config:

Terminal window
clonedb run --seed public.accounts --where "id = 123" --follow-children
clonedb run --seed public.orders --ids 1001,1002,1042 --scoped # bounded: just these + what they need
clonedb run --filtered-seed public.orders "status = 'open'" \
--filtered-seed public.users "id = 42" # per-seed predicates
clonedb run --scope checkout --scope smoke # union of two named scopes (bounded)
FlagDescription
--watch <INTERVAL>Re-run on an interval instead of exiting (e.g. 30s, 5m).
--exit-on-errorIn --watch mode, stop the loop on a transient run error instead of logging and continuing.

Each --watch iteration re-resolves credentials → runs the subset → sleeps → repeats, so short-lived cloud tokens (AWS RDS IAM, GCP IAM, Secret Manager) stay fresh across a long-running watch. Ctrl-C (SIGINT) or SIGTERM stops the loop cleanly and exits 0.

Terminal window
clonedb run --target "$DEV_DATABASE_URL" --watch 5m

Interval units: a bare number is seconds (10 == 10s); otherwise use ms, s, m, h, or d (and common long forms like min, hours, days).

FlagDescription
--parallel-readers <N>Number of parallel source reader connections for the run’s read phases (seed-key fetches, traversal batches, emit row fetches). Overrides [defaults] parallel_readers (default 4). Must be ≥ 1.

1 runs fully serial on a single connection; larger values open that many source connections for the read phases. Every reader adopts the run’s exported snapshot, so a parallel run reads the same point-in-time as a serial one — the result is identical, only faster. Values beyond the connection pool’s headroom are clamped at run time with a warning.

Answer “why is this table in my subset?” — or why seeding one row pulls in thousands. explain is a config-aware, schema-only diagnostic: it introspects the live FK graph, applies the same policy your run uses (seeds, mandatory upstream, cascade, exclude, relationship_overrides, follow_nullable), and reports how one table participates — without moving or counting a single row. Counts stay the job of run --dry-run; explain is the structure-and-policy lens.

Terminal window
clonedb explain public.users
clonedb explain public.users --from production
clonedb --json explain public.users # stable structured report for CI
FlagDescription
<TABLE>The schema-qualified table to explain (e.g. public.users). Required.
--from <SRC>Source connection — a connection name or a raw URL. Resolves like run’s source: name / URL → default_source → the source connection → $CLONEDB_SOURCE_URL.

It prints a verdict for the table under the effective config — the same three fates as reachable-fill:

  • SCOPED — at least one active seed reaches it, so it’s collected trimmed to the seed closure.
  • WHOLE — nothing reaches it from a seed under the current config, so it’s pulled entire. Add an FK (e.g. via config virtual-fks infer) or a restriction to scope it. This is the “everything is a full clone” signal, per table.
  • EXCLUDED (hard cut)exclude = true: never collected, regardless of how many edges point at it.

— then the direct (one-hop) edges that reach the table, in both directions, each annotated with its FK column(s) and traversal gate, and finally any cycle the table sits in.

explain public.users VERDICT: SCOPED
At least one active seed reaches this table under the current config, so it is
collected scoped to the seed closure.
Reached by (direct edges into public.users):
upstream (mandatory — referential integrity):
public.orders ──(user_id)──▶ public.users [mandatory]
downstream (cascade — only from a follow_children seed):
public.organizations ═(owner_id)═▶ public.users [follow_children]
⚠ Cycle (collecting any member collects the whole component):
{ public.organizations, public.users } LIVE
edges:
public.organizations.owner_id → public.users (nullable)
public.users.org_id → public.organizations
boundary candidates (NULL one of these FKs to break the cycle):
public.organizations.owner_id
Legend:
SCOPED reachable from a seed — collected scoped to the seed closure
WHOLE nothing reaches it from a seed — pulled whole (add an FK/restriction to scope it)
EXCLUDED exclude = true — never collected (a hard cut)
──▶ upstream parent edge (followed for referential integrity)
═▶ downstream child edge (followed only under an active cascade)
[mandatory] upstream parent — always collected for integrity
[follow_children] downstream child — collected because a cascade is active
[blocked by override] edge cut by a relationship_override (follow = false)
[skipped: nullable] nullable edge not followed (follow_nullable = false)
[not followed: expression virtual FK] a virtual FK whose PARENT column is a SQL expression (unsupported); child-side expressions ARE followed
[excluded ✗] the edge's source carries exclude = true (a hard cut)
⚠ LIVE cycle traversal through this cycle pulls the entire component

One hop at a time. explain shows only the direct neighbours that reach the target. To learn how one of those neighbours is itself reached, run explain on it — paths compose one hop per command, which is what keeps the output finite and readable even on a cyclic graph. An edge carries [virtual] when it comes from a virtual FK, and an edge tagged (source not collected) is dormant: the neighbour itself isn’t collected, so that edge never fires.

A cycle (a strongly-connected component, or a self-referential table like users.manager_id → users) is the classic explosion signature: collecting any member mandatorily collects the whole component, because upstream parents are always pulled for referential integrity. explain names the component, marks it LIVE when its edges are followed under the current config, and lists its boundary candidates — the nullable FK edges you could cut to bound the blast radius. NULL-ing one of those FKs (or severing it with a relationship_override) breaks the cycle. This previews the planned null-FK boundary feature, which will let clonedb make that cut automatically at the snapshot boundary.

VariableUsed byDescription
CLONEDB_SOURCE_URLevery command needing the sourceFull source connection URL, used when --source is omitted and no config-driven source provider applies.
CLONEDB_TARGET_URLrun --target (by convention)Conventional name for the target URL; the generated config’s [connections.target] reads it via the env provider.

Every command resolves its source the same way — through the named-connection pool. For run see Source & target (--from/--todefault_source/ default_target → a source/target connection → $CLONEDB_SOURCE_URL). sync-config, validate, and generate-config (when regenerating an existing config) resolve --source identically: a connection name or URL → default_source → the source connection → $CLONEDB_SOURCE_URL. All URLs are treated as secrets and stay out of argv and logs.

These variables are read from the process environment. clonedb doesn’t load a .env automatically — opt in with env_file or --env-file to populate the environment (including $CLONEDB_SOURCE_URL) from a file first.

CodeMeaning
0Success (including a clean Ctrl-C/SIGTERM stop of a --watch loop).
75EX_TEMPFAIL — in --watch, the credential provider reported credentials can no longer be refreshed (e.g. an expired AWS SSO session). A supervisor can match on this to re-authenticate and restart.
non-zeroAny other failure (bad config, connectivity, a run error under --exit-on-error, etc.).
# systemd: re-auth and restart when credentials expire mid-watch.
[Service]
RestartForceExitStatus=75