Every command. Every flag. Every exit code. Real output.

smugglr [GLOBAL FLAGS] <COMMAND> [FLAGS]

Global Flags

FlagDefaultDescription
--config <PATH>config.tomlPath to configuration file.
--verbose, -voffPrint detailed sync operations to stderr.
--output <FORMAT>textOutput format: text or json.

Sync Commands

push

Push local rows to the remote target. Only sends rows that actually changed (content-hashed delta).

smugglr push [--table NAME] [--dry-run]
FlagDefaultDescription
--table <NAME>all tablesSync a single table.
--dry-runoffShow what would be pushed without writing.
$ smugglr push --table users
smugglr: pushing users
  3 rows pushed (2 new, 1 updated)

Done. 3 rows pushed in 1.2s.

Dry-run output includes row counts per table without writing anything to the target.

$ smugglr push --dry-run
smugglr: dry run (no changes will be written)

  users:    3 rows to push (2 new, 1 updated)
  sessions: 0 rows to push

Total: 3 rows to push across 1 table.

pull

Pull remote rows to the local database. Only fetches rows that differ from what you already have.

smugglr pull [--table NAME] [--dry-run]
FlagDefaultDescription
--table <NAME>all tablesSync a single table.
--dry-runoffShow what would be pulled without writing.
$ smugglr pull --table users
smugglr: pulling users
  12 rows pulled (12 new, 0 updated)

Done. 12 rows pulled in 0.8s.

sync

Bidirectional sync. Pushes local changes to remote, pulls remote changes to local. Conflict resolution is controlled by the [sync] strategy in your config file, not by a CLI flag.

smugglr sync [--table NAME] [--dry-run]
FlagDefaultDescription
--table <NAME>all tablesSync a single table.
--dry-runoffShow what would happen in both directions.
$ smugglr sync
smugglr: syncing all tables
  users:    2 rows pushed, 5 rows pulled
  sessions: 0 rows pushed, 3 rows pulled

Done. 2 pushed, 8 pulled in 2.1s.

Note: conflict strategy (local_wins, remote_wins, newer_wins, uuid_v7_wins) is set in config.toml under [sync]. There is no --strategy flag.


diff

Read-only comparison between local and remote. Moves nothing. Shows what push, pull, or sync would do.

smugglr diff [--table NAME]
FlagDefaultDescription
--table <NAME>all tablesCompare a single table.
$ smugglr diff
smugglr: comparing local <-> remote

  users:
    local-only:  2 rows
    remote-only: 5 rows
    modified:    1 row

  sessions:
    local-only:  0 rows
    remote-only: 3 rows
    modified:    0 rows

This is a safe command. It reads both sides, hashes, and reports. Nothing is written anywhere.


Relay Commands

Stash and retrieve move data through S3-compatible storage (R2, MinIO, any S3 endpoint). The relay is a staging area for machines that cannot reach each other directly.

stash

Upload local database state to the S3 relay.

smugglr stash [--table NAME] [--dry-run]
FlagDefaultDescription
--table <NAME>all tablesStash a single table.
--dry-runoffShow what would be uploaded without writing.
$ smugglr stash
smugglr: stashing to relay (s3://smugglr-relay/default)
  users:    142 rows stashed
  sessions: 89 rows stashed

Done. 231 rows stashed in 3.4s.

Stash uses ETag-based concurrency protection. If another machine stashes the same table while you are uploading, the operation fails with exit code 4.


retrieve

Download from the S3 relay to the local database.

smugglr retrieve [--table NAME] [--dry-run]
FlagDefaultDescription
--table <NAME>all tablesRetrieve a single table.
--dry-runoffShow what would be downloaded without writing.
$ smugglr retrieve --table users
smugglr: retrieving from relay (s3://smugglr-relay/default)
  users: 142 rows retrieved (12 new, 3 updated)

Done. 142 rows retrieved in 1.9s.

Snapshot Commands

snapshot

Create a point-in-time backup of the local database state. Writes to the configured relay with a timestamped key.

smugglr snapshot [--dry-run]
FlagDefaultDescription
--dry-runoffShow snapshot metadata without writing.
$ smugglr snapshot
smugglr: creating snapshot
  timestamp: 2026-04-04T14:32:01Z
  tables:    3 (users, sessions, events)
  size:      2.4 MB

Snapshot saved to relay.

Snapshots are portable. Snapshot from D1, restore to Turso, or vice versa.


snapshots

List all available snapshots on the relay.

smugglr snapshots

No flags. Read-only.

$ smugglr snapshots
smugglr: 4 snapshots available

  2026-04-04T14:32:01Z  2.4 MB  3 tables
  2026-04-03T09:15:44Z  2.3 MB  3 tables
  2026-04-01T18:00:02Z  2.1 MB  2 tables
  2026-03-28T12:00:00Z  1.8 MB  2 tables

restore

Restore the local database from a snapshot. Requires the snapshot timestamp as an argument.

smugglr restore <TIMESTAMP> [--dry-run]
FlagDefaultDescription
--dry-runoffShow what the restore would do without writing.

The timestamp must match one returned by smugglr snapshots.

$ smugglr restore 2026-04-03T09:15:44Z
smugglr: restoring from snapshot 2026-04-03T09:15:44Z
  users:    134 rows restored
  sessions: 87 rows restored
  events:   412 rows restored

Done. 633 rows restored in 4.1s.

This overwrites local data. Use --dry-run first.


Daemon Commands

watch

Run smugglr as a background daemon. Syncs on a fixed interval until stopped.

smugglr watch [--interval SECS] [--dry-run]
FlagDefaultDescription
--interval <SECS>30Seconds between sync cycles.
--dry-runoffLog what each cycle would do without writing.
$ smugglr watch --interval 60
smugglr: watching (interval: 30s, strategy: local_wins)

[2026-04-04T14:32:01Z] users: 0 pushed, 2 pulled
[2026-04-04T14:33:01Z] no changes
[2026-04-04T14:34:01Z] users: 1 pushed, 0 pulled
^C
smugglr: stopped. 3 cycles completed.

Watch writes JSONL to stdout when --output json is set. Each tick is one JSON line. Useful for piping into log aggregators or agent workflows.

Stop with SIGINT (Ctrl+C) or SIGTERM. The daemon finishes the current cycle before exiting.


Network Commands

broadcast

Sync over the local network using encrypted UDP datagrams. No coordinator, no relay, no server. Machines with the shared encryption key receive and apply changes.

smugglr broadcast [--port PORT] [--interval SECS] [--once] [--dry-run]
FlagDefaultDescription
--port <PORT>config valueUDP port for broadcast.
--interval <SECS>config valueSeconds between broadcast cycles.
--onceoffBroadcast once and exit.
--dry-runoffShow what would be broadcast without sending.
$ smugglr broadcast --once
smugglr: broadcasting to 192.168.1.0/24:9847
  users:    142 rows (2 changed)
  sessions: 89 rows (0 changed)

Broadcast sent. 2 datagrams, 4.2 KB.

The encryption key is configured in [broadcast] in your config file. The key is the trust boundary: if you have the key, you are on the network. There is no authentication handshake.

Content hashing makes missed packets safe. If a machine misses a broadcast, the next one covers it. Idempotent by design.

For cross-network broadcast (machines not on the same LAN), bring your own tunnel: Tailscale, WireGuard, ZeroTier. Smugglr broadcasts to whatever subnet it sees.


Info Commands

status

Print configuration and connection status. Verifies that the config file is valid, the local database is readable, and the remote target is reachable.

smugglr status

No flags beyond the global ones.

$ smugglr status
--- Configuration ---
  Config file: loaded
  Local DB: ./local.db
  Target: Plugin (http-sql)
  Plugin path: /Users/you/.smugglr/plugins/smuggler-http-sql
  Timestamp column: updated_at
  Conflict resolution: LocalWins

--- Local Database ---
  Connection: OK
  Tables: 3
    users: 142 rows
    posts: 488 rows
    settings: 3 rows

--- Target Plugin (http-sql) ---
  Connection: OK
  Tables: 3
    users: 142 rows
    posts: 488 rows
    settings: 3 rows

There is no relay: line and no top-level strategy: line: the relay is not part of status, and the conflict strategy is reported as Conflict resolution. If the target cannot be reached, that section reports the failure (the process still exits non-zero):

$ smugglr status
--- Configuration ---
  Config file: loaded
  Local DB: ./local.db
  Target: Plugin (http-sql)
  Plugin path: /Users/you/.smugglr/plugins/smuggler-http-sql
  Timestamp column: updated_at
  Conflict resolution: LocalWins

--- Local Database ---
  Connection: OK
  Tables: 3
    users: 142 rows
    posts: 488 rows
    settings: 3 rows

--- Target Plugin (http-sql) ---
  Connection: FAILED - 401 Unauthorized. Check your API token.

Exit Codes

Every command returns a typed exit code. Use these for scripting, CI/CD gates, and agent workflows.

CodeNameMeaningRetry?
0SuccessOperation completed.No.
1General errorSomething unexpected broke.Maybe.
2Config errorBad config file, missing fields, invalid values.No. Fix the config.
3Network/transientConnection failed, timeout, HTTP 5xx.Yes, with backoff.
4ConflictConcurrent write detected (ETag mismatch, row conflict).Yes, after re-reading state.
5Not foundTable, snapshot, or resource does not exist.No. Check the name.
6Plugin errorA DataSource adapter failed.Depends on the adapter.

In shell scripts:

smugglr push
case $? in
  0) echo "done" ;;
  2) echo "fix your config" && exit 1 ;;
  3) echo "network issue, retrying..." && sleep 5 && smugglr push ;;
  4) echo "conflict, pulling first" && smugglr pull && smugglr push ;;
  *) echo "unexpected: $?" && exit 1 ;;
esac

JSON Output

Pass --output json to any command. Output follows a consistent CommandOutput structure.

Structure

{
  "command": "push",
  "status": "ok",
  "dry_run": false,
  "tables": [
    {
      "name": "users",
      "rows_pushed": 3
    }
  ]
}

In a table entry, rows_pushed and rows_pulled are omitted when zero. A push that touches no pulled rows will not include rows_pulled. Only tables with changes appear in tables.

Error output

{
  "command": "push",
  "status": "error",
  "error": "network timeout connecting to d1://my-database",
  "exit_code": 3
}

Error output always includes exit_code, matching the process exit status. See Exit Codes.

Dry-run output

Dry-run emits a distinct shape with status: "dry_run", top-level totals, and a per-table diff breakdown. There is no dry_run field. total_rows_to_push and total_rows_to_pull are both always present (they are zero when a direction has no work), as is exit_code.

{
  "command": "push",
  "status": "dry_run",
  "tables": [
    {
      "name": "users",
      "local_only": 2,
      "remote_only": 0,
      "local_newer": 1,
      "remote_newer": 0,
      "content_differs": 0,
      "identical": 139,
      "rows_to_push": 3,
      "rows_to_pull": 0
    }
  ],
  "total_rows_to_push": 3,
  "total_rows_to_pull": 0,
  "exit_code": 0
}

Every table entry carries all six diff counts (local_only, remote_only, local_newer, remote_newer, content_differs, identical) plus rows_to_push and rows_to_pull. For a pull, the push counts are zero and the pull counts carry the work; for sync, both directions are populated.

Watch output (JSONL)

When running smugglr watch --output json, each tick emits one JSON line to stdout:

{"command":"watch","tick":1,"status":"ok","tables":[{"name":"users","rows_pulled":2}]}
{"command":"watch","tick":2,"status":"ok","tables":[]}
{"command":"watch","tick":3,"status":"ok","tables":[{"name":"users","rows_pushed":1}]}

Each line is a WatchTickOutput object with a mandatory tick counter. The tables entries omit rows_pushed/rows_pulled when they are zero. Parse line by line.