Reference
Everything on this page is lookup material. If you're learning the tool, start with the Quickstart and How It Works instead.
Commands
| Command | What it does |
|---|---|
d6s profile add | Add or update a profile (name + URL) and optionally save a credential |
d6s profile test | Connect with a profile and print who you are on the instance |
d6s sync pull | Write a source instance's Schema and Configuration to sync files |
d6s sync diff | Show what a push would change on the target; applies nothing |
d6s sync push | Apply the sync files to a target instance |
d6s sync | Interactive 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:
| Flag | Effect |
|---|---|
--json | One machine-readable report on stdout; human status stays off stdout |
--no-color | Disable colored output |
--no-interactive | Disable 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]
| Flag | Effect |
|---|---|
--url <url> | Directus instance URL |
--token <token> | Static token to save to the credential store for this profile |
--yes | Skip 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 stored in project configuration.
d6s profile test
d6s profile test <name>
| Flag | Effect |
|---|---|
--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]
| Flag | Effect |
|---|---|
--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-schema | Skip 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 |
--all | Every configuration resource, including users |
--no-deps | Do not add prerequisites; resources owned by a selected parent remain included |
--project <name> | Project to sync (default: default) |
A bare pull includes every selectable resource except users. This means translations sync by default. Pass --no-translations to exclude them, or --translations to pull only translations.
The selectable resources are roles, policies, flows, dashboards, settings, folders, users, and translations. The CLI automatically includes access, permissions, operations, and panels with their parent resources (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 sync files point at something you omitted (a relation target, a group parent, a many-to-any collection). Pushing those files to a fresh target can fail; add the missing collections to
--collectionsyourself. - A name the server didn't return. A
--collectionsname absent from the returned Schema (usually a typo) is named in a warning. The partial sync files are still written, but never silently.
d6s sync diff
d6s sync diff --to <profile> [--mode <mode>] [--allow-version-drift]
| Flag | Effect |
|---|---|
--to <profile> (required) | Target profile name |
--mode <mode> | add, merge, or mirror; changes what the preview plans for |
--allow-version-drift | Preview 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]
| Flag | Effect |
|---|---|
--to <profile> (required) | Target profile name |
--mode <mode> | add, merge (default), or mirror |
--yes | Skip the apply confirmation; never authorizes deletions |
--dangerously-allow-delete | Consent to deletions; required for non-interactive mirror |
--allow-version-drift | Push 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:
| Key | Meaning | Default |
|---|---|---|
profiles | Named instances: { "url": "https://..." } per profile | {} |
directory | The directory pulls write into and pushes read from | "directus" |
projects | Per-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:
| Key | Meaning |
|---|---|
schema | false 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 |
collections | Schema scope: only these collections |
excludeCollections | Schema scope: all collections except these |
resources | Only these configuration resources |
excludeResources | The default resources except these |
mode | Default push mode for this project: add, merge, or mirror |
deps | false 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:
- A
--tokenflag, on the twoprofilecommands that accept one. The sync commands take no token flag. - The
DIRECTUS_<PROFILE>_TOKENenvironment variable: the profile name uppercased, soproductionreadsDIRECTUS_PRODUCTION_TOKEN. A.envfile next todirectus.config.jsonis loaded automatically without overriding real environment variables. - The credential store at
~/.directus/credentials.json, written readable only by you (mode0600). Never consulted when theCIenvironment 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 pull 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).
| Resource | In default pull | Select directly | Also includes | Notes |
|---|---|---|---|---|
roles | Yes | --roles | policies | |
policies | Yes | --policies | access, permissions | See the warning below before selecting policies on their own. |
access | Yes, with roles and policies | — | — | Grants attached to users are dropped when users are out of scope; a mirror push does not delete them on the target. |
permissions | Yes, with policies | — | — | Record counts are verified against the server. If the source hides records (unlicensed custom permission rules), the pull is incomplete. |
flows | Yes | --flows | operations | |
operations | Yes, with flows | — | — | |
dashboards | Yes | --dashboards | panels | |
panels | Yes, with dashboards | — | — | Panels have no field to match on, so a first push into a matching target can duplicate once; the ID map prevents repeats. |
settings | Yes | --settings | — | A single record. License and AI credentials, branding images (logos, backgrounds, favicon), and the default storage folder are stripped. |
folders | Yes | --folders | — | The media-library folder tree. Distinct from collection folders (Data Studio sidebar groups), which sync as Schema. |
translations | Yes | --translations | — | Matched by language and key; merge and mirror can update existing strings. |
users | Opt-in | --users | roles, policies | Secret fields (password, token, tfa_secret, and others) are stripped. |
--roles, not --policies alone
A selection that pulls policies without roles is not independently pushable when access records reference roles. With no roles in scope, a push to a fresh target fails. Select --roles instead; it includes policies and their dependent resources too.What a pull touches
Two rules govern every pull, scoped or not:
- A pull writes sync files only for what it fetched. Sync files for anything it did not fetch stay unchanged.
- A push only applies what is in the sync files. Work that never entered them cannot ship.
Written in this pull means the CLI fetched the current source state and wrote it to the corresponding sync files. Because writes are deterministic, an identical source state produces no git diff. Not written means the pull left those sync files unchanged on disk.
| Pull | Schema sync files | Configuration sync files |
|---|---|---|
pull --from staging | All written in this pull | Default set written; users not written |
... --collections posts | posts written; others not written | Default set written; users not written |
... --no-flows | All written in this pull | Flows, operations, and users not written; other defaults written |
... --no-translations | All written in this pull | Translations and users not written; other defaults written |
... --flows | All written (resource flags do not narrow Schema) | Flows and operations written; all others not written |
... --flows --no-schema | None written | Flows and operations written; all others not written |
... --collections posts --no-flows | posts written; others not written | Flows, operations, and users not written; other defaults written |
Push modes
| Mode | Schema | Configuration | Deletes? |
|---|---|---|---|
add | Adds and modifies, same as merge | Creates only; existing records are never updated | No |
merge (default) | Adds and modifies | Creates and updates | No |
mirror | May delete in scope | Creates, updates, and deletes records absent from the sync files | Yes, gated |
A mirror push deletes only within what the sync files cover: a pull scoped to some collections can delete fields inside those collections, never a collection it doesn't contain. A pull the source itself left incomplete (hidden permission records) is refused at mirror push outright.
Deletion gates
Only mirror deletes, and deleting always requires its own explicit consent:
| Context | To delete you must |
|---|---|
| Interactive terminal | Review the plan naming the losses, then type the profile name (unless you passed --dangerously-allow-delete, which is the consent) |
| Non-interactive / CI | Pass --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 configuration records absent from the commit-ready files; pass --dangerously-allow-delete to consent, or use --mode merge.
Record identity
Records are matched across instances first by the ID map (<directory>/<project>/id_map.json), then by an identifying field:
| Resource | Matched by |
|---|---|
| Most resources | name |
| Users | email |
| Operations | key |
| Translations | language and key |
| Panels | Nothing; ID map only |
The ID map is keyed internally by source and target instance URL, so one 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 Directus version recorded in the sync files 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 pull the source cut short. The JSON API keeps data as its compatibility field name; it contains the Configuration report.
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 sync files and the instance disagree: version mismatch, changed target Schema, incomplete pull), 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 Configuration 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. 🐰