Reference

Every Environment Sync command and flag, the directus.config.json format, credential resolution, scope tables, push modes, deletion gates, and JSON report formats.

Everything on this page is lookup material. If you're learning the tool, start with the Quickstart and How It Works instead.

Commands

CommandWhat it does
d6s profile addAdd or update a profile (name + URL) and optionally save a credential
d6s profile testConnect with a profile and print who you are on the instance
d6s sync pullWrite a source instance's schema and configuration to committable files
d6s sync diffShow what a push would change on the target; applies nothing
d6s sync pushApply the committed files to a target instance
d6s syncInteractive wizard: prompts for source, target, project, and mode, then pulls and pushes

The CLI installs as directus-cli with d6s as an equivalent short alias.

Global flags, available on every command:

FlagEffect
--jsonOne machine-readable report on stdout; human status stays off stdout
--no-colorDisable colored output
--no-interactiveDisable prompts; behave as in CI
--config <path>Path to directus.config.json (default: found by walking up from the current directory)

d6s profile add

d6s profile add [name] [--url <url>] [--token <token>] [--yes]
FlagEffect
--url <url>Directus instance URL
--token <token>Static token to save to the credential store for this profile
--yesSkip the confirmation when repointing an existing profile to a new URL

Adding is an upsert: an existing name is updated. Run without arguments for prompts, which also offer to save a credential (paste a static token, or log in with email and password to save a session; saved sessions refresh themselves before they expire). Profile names use letters, numbers, and underscores. URLs with embedded credentials, query strings, or fragments are refused, because the URL is the part that lands in a committed file.

d6s profile test

d6s profile test <name>
FlagEffect
--url <url>Test a URL directly, without a profile or config file
--token <token>Override the resolved token

Connects and prints who the credential authenticates as:

◇ Authenticated to https://cms.example.com as Admin User (Administrator).

d6s sync pull

d6s sync pull --from <profile> [scope flags]
FlagEffect
--from <profile> (required)Source profile name
--collections <list>Schema scope: only these collections (comma-separated)
--exclude-collections <list>Schema scope: all collections except these
--no-schemaSkip the schema entirely; configuration resources only
--<resource>Select only the named resources, e.g. --flows --roles (plus their dependencies)
--no-<resource>Keep the default resource set but exclude one, e.g. --no-flows
--allEvery configuration resource, including users and translations
--no-depsDo not pull a selected resource's dependencies (dependent children still ride with their parent)
--project <name>Project to sync (default: default)

The selectable resources are roles, policies, flows, dashboards, settings, folders, users, and translations; access, permissions, operations, and panels ride along with their parents (see the dependency table). Positive selection (--flows) cannot be combined with --all or with --no- flags. Resource selection never narrows the schema; the two axes are scoped independently.

Two warnings a scoped pull can raise, neither of which widens the scope for you:

  • Out-of-scope references. The scoped snapshot points at something you omitted (a relation target, a group parent, a many-to-any collection). Pushing that snapshot to a fresh target can fail; add the missing collections to --collections yourself.
  • A name the server didn't return. A --collections name absent from the returned snapshot (usually a typo) is named in a warning. The partial snapshot still lands, but never silently.

d6s sync diff

d6s sync diff --to <profile> [--mode <mode>] [--allow-version-drift]
FlagEffect
--to <profile> (required)Target profile name
--mode <mode>add, merge, or mirror; changes what the preview plans for
--allow-version-driftPreview despite a version mismatch (see the version rule)
--project <name>Project to sync (default: default)

Applies nothing, and exits 0 whether or not differences exist; automation reads the report's changes field.

d6s sync push

d6s sync push --to <profile> [--mode <mode>] [--yes] [--dangerously-allow-delete] [--allow-version-drift]
FlagEffect
--to <profile> (required)Target profile name
--mode <mode>add, merge (default), or mirror
--yesSkip the apply confirmation; never authorizes deletions
--dangerously-allow-deleteConsent to deletions; required for non-interactive mirror
--allow-version-driftPush despite a version mismatch (see the version rule)
--project <name>Project to sync (default: default)

directus.config.json

Created and updated by d6s profile add; found by walking up from the current directory, like git finds .git. It never contains credentials, so commit it. The full shape:

{
  "profiles": {
    "staging": { "url": "https://staging.example.com" },
    "production": { "url": "https://cms.example.com" }
  },
  "directory": "directus",
  "projects": {
    "default": {
      "schema": true,
      "collections": ["pages", "posts"],
      "resources": ["flows", "settings"],
      "mode": "merge"
    }
  }
}

Top-level keys:

KeyMeaningDefault
profilesNamed instances: { "url": "https://..." } per profile{}
directoryThe directory pulls write into and pushes read from"directus"
projectsPer-project sync options (see below){}

Per-project keys, all optional. A project is a named slice of the sync with its own subdirectory (<directory>/<project>/); the default project exists without being declared. Flags on the command line override these per run:

KeyMeaning
schemafalse makes this a configuration-only project: pull, diff, and push never touch schema, and reports say schemaSkipped so automation can tell a skipped phase from a matching one
collectionsSchema scope: only these collections
excludeCollectionsSchema scope: all collections except these
resourcesOnly these configuration resources
excludeResourcesThe default resources except these
modeDefault push mode for this project: add, merge, or mirror
depsfalse skips pulling selected resources' dependencies

"schema": false combined with a collections scope is refused as a contradiction, as is setting both an include and an exclude list for the same axis. Project names use letters, numbers, hyphens, and underscores.

Credentials

When a command authenticates a profile, the token resolves in order:

  1. A --token flag, on the two profile commands that accept one. The sync commands take no token flag.
  2. The DIRECTUS_<PROFILE>_TOKEN environment variable: the profile name uppercased, so production reads DIRECTUS_PRODUCTION_TOKEN. A .env file next to directus.config.json is loaded automatically without overriding real environment variables.
  3. The credential store at ~/.directus/credentials.json, written readable only by you (mode 0600). Never consulted when the CI environment variable is set.

Use an admin credential. The schema endpoints and the batch import the CLI relies on are admin-only on the server; the CLI does not check privileges up front, so a non-admin token fails with an authentication error, or produces an incomplete export that the completeness checks then flag.

Configuration resources

Resource selection follows a dependency graph: selecting a resource pulls in what it needs (unless --no-deps).

ResourceIn default pullSelect directlyPulls inNotes
rolesYes--rolespolicies
policiesYes--policiesaccess, permissionsSee the warning below before selecting policies on their own.
accessYes, with roles and policiesGrants attached to users are dropped when users are out of scope; a mirror push does not delete them on the target.
permissionsYes, with policiesRow counts are verified against the server. If the source hides rows (unlicensed custom permission rules), the export is marked incomplete.
flowsYes--flowsoperations
operationsYes, with flows
dashboardsYes--dashboardspanels
panelsYes, with dashboardsPanels have no field to match on, so a first push into a matching target can duplicate once; the identity map prevents repeats.
settingsYes--settingsA single record. License and AI credentials, branding images (logos, backgrounds, favicon), and the default storage folder are stripped.
foldersYes--foldersThe media-library folder tree. Distinct from collection folders (Data Studio sidebar groups), which sync as schema.
usersOpt-in--usersroles, policiesSecret columns (password, token, tfa_secret, and others) are stripped.
translationsOpt-in--translationsMirror pushes of translations are not currently supported, which is why they are opt-in.
Select --roles, not --policies alone A selection that pulls policies without roles is not independently pushable when access rows reference roles: access rows carry references to roles, and with no roles in scope a push to a fresh target fails. Select --roles instead; it pulls policies and their children too.

What a pull touches

Two rules govern every pull, scoped or not:

  1. A pull only rewrites what it fetched. Everything it did not fetch keeps its committed state, untouched.
  2. A push only applies what is in the files. Work that never entered them cannot ship.

Refreshed means the file is rewritten from the source; because writes are deterministic, an unchanged resource produces no git diff. Preserved means the file is not touched at all.

PullSchema filesConfiguration files
pull --from stagingAll refreshedAll refreshed
... --collections postsposts refreshed, others preservedAll refreshed
... --no-flowsAll refreshedFlows preserved, others refreshed
... --flowsAll refreshed (resource selection never narrows schema)Flows and operations refreshed, others preserved
... --flows --no-schemaAll preservedFlows and operations refreshed, others preserved
... --collections posts --no-flowsposts refreshed, others preservedFlows preserved, others refreshed

Push modes

ModeSchemaDataDeletes?
addAdds and modifies, same as mergeInserts only; existing records are never updatedNo
merge (default)Adds and modifiesCreates and updatesNo
mirrorMay delete in scopeCreates, updates, and deletes records absent from the filesYes, gated

A mirror push deletes only within what the committed files cover: a snapshot scoped to some collections can delete fields inside those collections, never a collection it doesn't contain. An export the source itself left incomplete (hidden permission rows) is refused at mirror push outright.

Deletion gates

Only mirror deletes, and deleting always requires its own explicit consent:

ContextTo delete you must
Interactive terminalReview the plan naming the losses, then type the profile name (unless you passed --dangerously-allow-delete, which is the consent)
Non-interactive / CIPass --dangerously-allow-delete

--yes skips the ordinary confirmation prompt, but it never authorizes a deletion; that holds even if a supposedly additive push unexpectedly carries one. A mirror push in CI without --dangerously-allow-delete refuses before changing anything:

✖ Refusing mirror mode in a non-interactive context without --dangerously-allow-delete.
  mirror can delete schema and data rows absent from the import set; pass --dangerously-allow-delete to consent, or use --mode merge.

Record identity

Records are matched across instances first by the committed identity map (<directory>/<project>/id_map.json), then by an identifying field:

ResourceMatched by
Most resourcesname
Usersemail
Operationskey
PanelsNothing; identity map only

The map is keyed internally by source and target instance URL, so one committed file serves any number of targets without conflicts. Commit it whenever a push changes it. An ambiguous match (two candidates) prompts interactively and refuses non-interactively:

✖ Ambiguous target matches:
  directus_roles source "Editor" — sr1 → one of t1, t2
  Run d6s sync push interactively once to choose, then commit the updated id map.

The version rule

Schema changes require the snapshot's Directus version and the target's version to match exactly, patch release included; the mismatch error names both versions. --allow-version-drift proceeds anyway with a warning; the CLI never translates schema between versions. Projects with "schema": false skip the check entirely, and a target whose version cannot be read is left to the server's own gate rather than refused.

JSON reports

With --json, stdout carries exactly one report per command; warnings still go to stderr so logs keep them while stdout stays parseable. Reports are emitted as a single line; they're formatted here for readability.

d6s sync pull --from staging --json:

{
  "kind": "PullReport",
  "formatVersion": 1,
  "ok": true,
  "source": "https://staging.example.com",
  "profile": "staging",
  "project": "default",
  "schemaSkipped": false,
  "dir": "directus/default/schema",
  "collections": 12,
  "fields": 87,
  "systemFields": 2,
  "relations": 14,
  "files": 13,
  "removed": [],
  "scope": null,
  "data": {
    "resources": ["directus_flows", "directus_roles"],
    "collections": 10,
    "records": 57,
    "files": 10,
    "removed": [],
    "incomplete": []
  }
}

The schema counters (collections through files) are null when the schema phase is skipped; scope echoes a --collections/--exclude-collections scope; data.incomplete names resources whose export the source cut short.

d6s sync diff --to production --json:

{
  "kind": "DiffReport",
  "formatVersion": 1,
  "ok": true,
  "target": "https://cms.example.com",
  "profile": "production",
  "project": "default",
  "mode": "merge",
  "changes": true,
  "unresolved": 0,
  "schemaSkipped": false,
  "added": 1,
  "modified": 1,
  "deleted": 0,
  "data": {
    "mode": "merge",
    "source": "https://staging.example.com",
    "collections": {
      "directus_flows": {
        "existing": [],
        "new": ["f1"],
        "deleted": [],
        "mapped": {}
      }
    },
    "matched": 1,
    "ambiguous": 0,
    "unmatched": 1,
    "unchanged": 0,
    "incomplete": [],
    "skipped": false
  }
}

changes is true when a push would do anything, including when records are unresolved; added/modified/deleted count schema items; data.collections is the target server's own per-collection dry-run answer.

d6s sync push --to production --yes --json reports the same shape as a diff, with applied (true when the push changed the target) in place of unresolved, and data.collections reflecting what the import actually did.

Failures put an error report on stdout:

{
  "kind": "ErrorReport",
  "formatVersion": 1,
  "error": {
    "code": "STATE",
    "message": "Version mismatch: the snapshot was pulled from Directus 11.2.0, but the target runs 11.2.5.",
    "hint": "..."
  }
}

The code is one of a small set of failure classes: USAGE (the command line needs fixing: a missing flag or missing consent), CONFIG (directus.config.json missing or invalid), AUTH (the credential was rejected), HTTP (the instance could not be reached or returned an error), STATE (the committed files and the instance disagree: version mismatch, changed target schema, incomplete export), or UNKNOWN. Exit codes are 0 for success and 1 for every failure; the code string is the finer-grained signal.

Output conventions

Human-readable status lines go to stderr, prefixed (info), (success), (warning), or (error, with its hint indented beneath). Plan lines go to stdout: + marks an addition, ~ a modification, and ✖ DELETE a deletion, with data plans summarized per collection as +N new ~N updated ✖N deleted. --no-color disables coloring; --json replaces stdout output with the report while warnings stay on stderr.

Get once-a-month release notes & real‑world code tips...no fluff. 🐰