Configuration
Project config files, Atlas config subset, environment variables, and precedence.
Configuration precedence is:
| Rank | Source |
|---|---|
| 1 | Explicit CLI flags |
| 2 | Environment variables |
| 3 | atlas.hcl selected environment |
| 4 | ptah.yaml selected environment |
| 5 | Built-in defaults |
A PTAH_<FLAG> value must parse as the corresponding flag type. Ptah rejects a
malformed value before argument validation, command hooks, or database work
begins. For example, PTAH_DRY_RUN=notabool fails with
invalid boolean value "notabool" for PTAH_DRY_RUN instead of running with
the default false value. An explicit CLI flag wins without reading its
environment twin, including when that environment value is malformed. An empty
value remains unset for every flag type except boolean, where it is rejected;
see Boolean environment variables.
The flags with no environment variable
Section titled “The flags with no environment variable”A few flags do not bind to a PTAH_* variable. They have to be typed, or
written into a command line somebody wrote:
| flag | on |
|---|---|
--auto-approve |
ptah schema apply, ptah db drop-all, ptah mcp, ptah assist, ptah assist explain, ptah assist context |
--allow-database-inspect |
ptah mcp, ptah assist, ptah assist explain, ptah assist context |
--db-url |
ptah schema test, ptah migrations test |
--json |
ptah schema plan, ptah schema apply |
Every other flag in the native CLI binds to one.
The approval flags are the first two. --db-url on the two test verbs is the
third, for a neighboring reason: those verbs mutate the database they are given
and treat it as disposable. Bound to the environment, an operator with
PTAH_DB_URL exported for ordinary work runs the suite against that database by
omitting a flag. Requiring the flag costs nothing, because a throwaway target is
a decision rather than a default.
That is deliberate, and it is the reason every other flag can safely have one: a container, a CI job, or a Kubernetes Job can be configured entirely through the environment, and none of that configuration can turn a review step into an unattended apply. A variable exported once in a shell profile would otherwise approve every later run in that shell.
A Kubernetes Job passes it in args: rather than env:, which is the same
distinction: what the workload is versus what it is configured with.
--json on the two schema verbs decides what standard output is. PTAH_JSON
is the variable the versioned commands read, and bound here, a value exported
for ptah migrations up --json would turn every plan preview and apply
transcript in that environment into a JSON document. A caller that parses the
document types the flag.
Boolean environment variables
Section titled “Boolean environment variables”Every boolean PTAH_* variable follows one rule, whether it is a flag’s
environment twin or a feature toggle read directly:
| State | Result |
|---|---|
| Not set | The documented default |
| Set to a valid boolean | Parsed and honored |
| Set to anything else | The command fails before doing any work |
The accepted spellings are Go’s strconv.ParseBool spellings:
true: 1 t T true True TRUEfalse: 0 f F false False FALSENothing is trimmed before parsing. " true", "true " and an exported empty
value are all rejected, because a quoting or expansion mistake in a YAML
manifest, a CI environment file or a systemd unit is exactly the configuration
error this rule exists to surface. The refusal names the variable and the raw
value:
Error: invalid boolean value "maybe" for PTAH_POSTGRES_INSPECT_ALL_ROLESThe refusal comes before the command connects to a database, writes a migration
file, applies schema changes, touches revision state, or produces a
machine-readable result on standard output. It also comes before the command’s
own early returns: an invalid value is rejected even on a run that would never
have consulted it, such as a migrate lint that already names --latest, or a
schema apply whose --exclude selectors all match.
A command only validates the variables it owns. A malformed PostgreSQL
inspection variable does not break an unrelated SQLite command or
ptah version.
Non-boolean PTAH_* variables are unchanged: an empty value still reads as
unset for them.
Atlas CE strict compatibility
Section titled “Atlas CE strict compatibility”PTAH_ATLAS_STRICT_COMPAT is a ptah-compat-only boolean policy selector. It
defaults to false, which keeps the complete Pro-like and best-effort
compatibility surface. Set it to true only for Atlas Community Edition oracle
or conformance runs:
PTAH_ATLAS_STRICT_COMPAT=1 ptah-compat migrate validate --dir file://migrationsThe process resolves the selector before command construction. In strict mode,
generic PTAH_<FLAG> environment twins are not installed because Atlas CE does
not expose them. A present extension variable, including one whose command flag
is absent from the strict tree, is rejected rather than ignored. False values
for known boolean extension toggles remain valid; malformed values retain the
normal boolean diagnostic.
The check is based on Ptah’s actual flag bindings and feature-toggle registry,
not on the PTAH_ prefix alone. An ordinary value that an atlas.hcl reads
through getenv, such as PTAH_DATABASE_PASSWORD, remains available in strict
mode. Naming project inputs with that prefix does not turn them into Ptah
features.
Strict mode also applies its object inventory to live schemas. schema inspect,
schema apply, schema diff, and schema clean refuse a Pro-only object
before rendering, comparison, or mutation instead of copying CE behavior that
could omit, miscompare, or destroy it. On schema clean, the CE-registered
--dry-run and --format flags remain gated. The diagnostic names
ptah-compat and tells the operator to unset the selector; Ptah never directs
the operator to install Atlas. Leave strict mode off for Ptah’s complete
cleanup, inspection, diff, and apply capabilities.
Strict inspection omits PostgreSQL’s server-installed plpgsql extension and
baseline PUBLIC USAGE grant from rendered output. Strict cleanup executes the
validated plan after confirmation. On PostgreSQL it locks the planned tables,
revalidates the live inventory, compares the rebuilt cleanup plan with the
confirmed plan, and refuses drift before the first drop. Full mode preserves
the complete reader output and its established unscoped writer path.
The cleanup check covers the writer’s complete destruction inventory. That includes PostgreSQL procedures, aggregates, foreign tables, collations, default privileges, and dependent objects such as triggers that disappear with a table without appearing as a separate cleanup plan line.
Strict schema workflows also refuse YAML sources and an authored schema apply
lint policy that the CE execution path cannot enforce. Commands that execute,
convert, or replay migration bodies refuse Atlas txtar, every Ptah directive,
and SQL templates; checksum-only reads preserve those bytes. All inputs remain
available in the default, complete compatibility profile.
Which variables strict mode gates is derived from Ptah’s boolean-variable registry, not from a list kept beside it. Each variable states at its single declaration site whether strict mode gates it or retains it, so a variable added without stating anything is refused rather than ignored, and there is no second list to forget.
These opt-in correctness controls remain available because they do not add an
Atlas capability: PTAH_ALLOW_EXTERNAL_TEST_COMMAND,
PTAH_ALLOW_NONINTERACTIVE_EDIT, PTAH_ALLOW_SERVER_CLEAN,
PTAH_ATLAS_ALLOW_UNMATCHED_EXCLUDE, PTAH_DEV_SERVER_DISPOSABLE,
PTAH_HCL_STRICT_REDECLARATIONS,
PTAH_SQLITE_ALLOW_UNREGISTERED_VIRTUAL_MODULE,
PTAH_SQLITE_ALLOW_VIRTUAL_TABLE_DROP and PTAH_STRICT_DIR_QUERY.
PTAH_ALLOW_EXTERNAL_TEST_COMMAND is the older spelling of the permission to
run an external step in a .test.hcl case. ptah-compat runs the step as
the pinned community binary does and needs no variable. Native ptah runs it
with --allow-config-exec, or with this variable, which still authorizes
external steps and nothing else. See
Programs a project file names.
The program is executed as an argument vector with no shell, and a refused run stops before any database is provisioned.
PTAH_ALLOW_NONINTERACTIVE_EDIT permits a scripted editor in a non-interactive
process; it does not add an editor or migration capability.
PTAH_ALLOW_SERVER_CLEAN lets schema clean drop every user database of a
whole MySQL or MariaDB server, which the pinned community binary does without
asking; Ptah refuses it by default.
PTAH_DEV_SERVER_DISPOSABLE declares the --dev-url server the run’s own, so
replay, and the rehearsal of schema apply and of a plan file, run a DO
block, a routine or a role on it; the pinned community binary runs those on any
dev database. See
a server declared disposable.
PTAH_SQLITE_ALLOW_VIRTUAL_TABLE_DROP restores the DROP TABLE the pinned
community binary plans for a SQLite virtual table anyway, and
PTAH_SQLITE_ALLOW_UNREGISTERED_VIRTUAL_MODULE lifts a refusal that is Ptah’s
own, since the binary has no notion of a module this build cannot classify and
plans the drops regardless. Gating either would make strict mode the one place
Ptah is stricter than the binary it matches. Every other declared
boolean is gated, and a malformed value for any of them fails the process
whatever its classification. Native ptah does not read
PTAH_ATLAS_STRICT_COMPAT.
Project-file merging preserves source presence. For a supported field, an
explicitly present value replaces the lower-precedence value instead of being
treated as absent. This includes an empty string, zero, false, or an empty
list when the field accepts that type. Thus atlas.hcl wins over ptah.yaml,
while environment variables and explicit CLI flags still win.
After project sources are merged, a command applies its built-in default only
when a field is absent. An explicitly present empty or zero value instead
reaches normal validation. Fields that do not accept empty values, including
Atlas format templates, fail during parsing or command validation.
Use ptah.yaml for Ptah-owned configuration and the supported atlas.hcl
subset for Atlas-compatible project config. The supported Atlas subset includes
local variable defaults and Atlas-style --var name=value overrides,
locals, getenv, file, fileset, data.hcl_schema, data.sql,
data.external, data.runtimevar, and data.template_dir. Recognized project
data sources are evaluated only when the selected config references them.
Supported Atlas env blocks can also set schema.src, schema.mode, format,
and local diff policy defaults for the ptah-compat binary’s commands.
ptah-compat migrate apply expands env for_each collections and applies each
selected database target sequentially.
Ptah reads each selected project config once per command and converts it to a
typed configuration value. Migration database settings and online-DDL policy
therefore cannot come from different generations of a concurrently replaced
ptah.yaml. When an Atlas-compatible adapter delegates to a native command
that also consumes project configuration, the adapter passes the merged typed
snapshot instead of letting that command reopen either project file. This
currently applies to migrate down; other adapters evaluate Atlas project
configuration once and map supported values to explicit native command
arguments. An explicit --config path must exist; the conventional
./ptah.yaml is optional.
A ptah.yaml that sets nothing is an empty
configuration, as a missing one is: a file of zero bytes, one holding only
comments and blank lines, ---, null and {} all leave every setting at its
default. The file is one YAML document. A later document after --- that sets
anything is refused with the line it starts on, because Ptah would not read it;
a trailing --- with nothing after it is accepted.
--config takes a ptah.yaml file. Pointing it at an atlas.hcl is refused by
name rather than reported as a YAML parse failure: the Atlas project config is
discovered as ./atlas.hcl and selected with --env.
Every native command that accepts --env also accepts --var name=value, which
supplies a value for an atlas.hcl variable block that declares no default.
The flag is repeatable, and repeating one name builds a list(string). This is
the flag the evaluator names when a variable cannot be resolved:
$ ptah schema compare --env local --schema-file schema.sqlerror: atlas.hcl variable "dburl" requires a default or --var dburl=value$ ptah schema compare --env local --schema-file schema.sql --var dburl=sqlite://app.dbenv:// references resolve on the ptah-compat --to and --from flags. The
native --schema-file does not resolve them and says so, naming env:// and,
for an attribute outside src, schema.src, url, dev, migration, and
migration.dir, naming the attribute.
For Atlas-compatible commands, plain local schema paths, relative file://
schema URLs, and relative migration.dir values declared in atlas.hcl resolve
relative to the directory containing that atlas.hcl file. Explicit CLI path
flags such as --to, --from, and --dir keep CLI semantics and resolve
relative to the process working directory unless they are absolute.
A project-configured migration.dir, whether relative or absolute, must remain
inside the directory containing atlas.hcl after symbolic-link resolution.
Parent traversal that resolves outside, an absolute outside path, and a
symbolic-link escape fail as outside allowed root. An explicit CLI --dir is
operator-owned and retains the CLI path behavior above.
Two files, one project model
Section titled “Two files, one project model”ptah.yaml and the supported atlas.hcl subset are both native Ptah project
languages. Neither is an import format for the other: each is parsed into the
same project-config model, and every native ptah verb that reads project
configuration reads either file. A project written in HCL needs no conversion
to YAML to be operated by native Ptah, and one written in YAML needs no HCL.
What differs is reach, not standing. The Atlas subset is bounded by what the
compatibility profile covers, so a setting only Ptah has is expressed in
ptah.yaml; Ptah’s own HCL extensions live in the schema language rather than
the project file, and are listed in HCL schema. A project
using one of those is no longer a project Atlas reads.
Both files can be present at once, and are merged by the precedence above. Adopting an Atlas project walks the path that uses this.
A minimal project config
Section titled “A minimal project config”A native command selects an environment with --env, and reads it from either
project file: ./ptah.yaml, or the supported subset of ./atlas.hcl. The two
below are equivalent, and ptah migrations status --env dev answers the same
way beside each.
env: dev: url: sqlite:////tmp/ptah-dev.db migration: dir: ./migrationsenv "dev" { url = "sqlite:////tmp/ptah-dev.db"
migration { dir = "file://migrations" }}Run with the named environment:
ptah migrations status --env devptah migrations up --env dev --verify-sumThe two files are not interchangeable everywhere: --config names a
ptah.yaml and refuses an atlas.hcl by name, because the Atlas project
config is discovered as ./atlas.hcl and selected with --env. Where both
exist, atlas.hcl wins — see the precedence table above.
If a command needs project values from a config file with multiple
environments, pass --env. Commands whose required inputs are all explicit do
not need to select an environment. Ptah fails instead of guessing only when
project values are required and the environment remains ambiguous.
Operational settings
Section titled “Operational settings”Project config can also define timeouts, revision table layout, migration directory format, transaction mode, backup destinations, pre-flight hooks, webhooks, lint defaults, and online-DDL policy.
The dev value supplies the disposable database for migration generation and
the shadow rehearsal that migrations down performs before touching its target.
| Setting area | Example keys |
|---|---|
| Database target | url, src, schema.src, dev, schemas |
| External desired schema | external_schema.program, external_schema.format, external_schema.working_dir, external_schema.env |
| Migration directory and revisions | migration.dir, migration.format, migration.runner, migration.revisions_table, migration.revision_format |
| Safety and operations | migration.pre_up_hook, migration.pg_dump_to, migration.webhook, migration.exec_order, migration.tx_mode |
| Lint defaults and policy | lint.dialect, lint.disabled-rules, lint.latest, lint.git.base, lint.destructive.error, lint.concurrent_index.error, lint.naming |
| Online DDL | online_ddl.tool, online_ddl.threshold_rows, online_ddl.args, online_ddl.fallback |
| Diff policy | diff.skip: [drop_table, drop_column, drop_index, drop_enum], diff.concurrent_index, diff.concurrent_index_drop, diff.online_alter |
| Atlas-compatible output | format.schema.inspect, format.schema.apply, format.schema.clean, format.schema.diff, format.migrate.apply, format.migrate.diff, format.migrate.lint, format.migrate.status |
| Atlas-compatible diff policy | diff.skip.drop_table, diff.skip.drop_column, diff.skip.drop_index, diff.concurrent_index.create, diff.concurrent_index.drop |
The diff block shapes what ptah migrations generate writes and what
ptah schema apply, schema plan and schema diff plan. diff.skip lists
destructive change kinds (drop_table, drop_column, drop_index,
drop_enum) to omit. The generated migration, or the plan, opens with one
-- SKIP: ... comment for each change it left out:
-- SKIP: DROP COLUMN of t.b omitted by diff policy (skip: drop_column)When the policy leaves nothing to run, the same line goes to standard error
beside Schema is synced. The commands share one filter, so they omit the
same changes. A skipped column stays in its table’s declaration: a SQLite table
rebuilt for another change, and a table a provider plans as a whole, keep the
column and its rows. ptah-compat migrate diff writes no comment into the
migration file, which is what atlas.sum hashes; it reports the omissions on
standard error.
diff.concurrent_index: true requests CREATE INDEX CONCURRENTLY for new
indexes (PostgreSQL, capability-gated), while
diff.concurrent_index_drop: true requests DROP INDEX CONCURRENTLY for
standalone index removals under the same gate. A skipped change is never
emitted, so it never trips the --check-destructive gate.
diff.online_alter: true asks the server to apply the plan without blocking
the writes already running against its tables. Each engine family is asked in
its own grammar, and a target with neither is planned as if the setting were
off.
- MySQL and MariaDB: every
ALTER TABLEcarriesALGORITHM=INPLACE, LOCK=NONE. The server refuses the statement when it cannot honor it, so a change that would copy the table fails at the statement rather than proceeding under a copy nobody asked about, and the refusal is reported as the server declining to do it online. - PostgreSQL: there is no such clause, so the safe form is generated. A
CHECKorFOREIGN KEYconstraint is addedNOT VALIDand completed by its ownVALIDATE CONSTRAINT, whose scan takesSHARE UPDATE EXCLUSIVErather thanACCESS EXCLUSIVE. A constraint the declaration allowsNOT VALIDis added that way with or without this setting, and is not validated.
The PostgreSQL pair buys that weaker lock only when the two statements commit
separately, so a generated migration carrying one runs outside a transaction. A
plan that mixes it with ordinary statements is refused: the constraint change
belongs in a migration of its own. ptah schema apply executes the statements
itself, so it refuses the setting unless it runs with --tx-mode none, rather
than holding the addition’s lock through the validation scan.
On MySQL and MariaDB a table rename is refused under this setting, and that is
the server’s answer rather than Ptah’s: measured on MySQL 8.4.6 and MariaDB
12.3.3, ALTER TABLE ... RENAME TO ..., ALGORITHM=INPLACE, LOCK=NONE returns
1845 asking for LOCK=EXCLUSIVE. A column rename and a table comment are
accepted. A selected environment’s
diff.skip replaces the top-level list; an explicit empty list clears all
inherited skip kinds.
drop_table, drop_column and drop_index also reach the comparison, not
only the rendered SQL. Ptah’s SQLite virtual-table guard refuses a comparison
for the statements it predicts, and a project that skips those drops has
already deleted them, so the refusal does not fire — see
SQLite. They change what a comparison refuses and
nothing about what it computes.
The Atlas-compatible command tree lives in the separate ptah-compat binary,
the drop-in replacement for scripts that expect Atlas-style root commands.
Atlas project flags such as --config, -c, --env, and repeated
--var name=value belong to that tree only; it is not a separate
configuration surface.
External desired schema
Section titled “External desired schema”Use external_schema when an ORM, framework, or generator owns the desired
schema:
external_schema: program: [".venv/bin/atlas-provider-sqlalchemy", "--path", "./models", "--dialect", "postgresql"] format: sql working_dir: ./app env: ["APP_ENV=dev"]program is an explicit argument list and is executed without a shell.
format accepts sql (the default), hcl, or yaml. The block supplies the
desired schema to native schema render, schema compare, schema drift,
migrations plan, and migrations generate commands when --schema-cmd is
not set. An explicit command and its --schema-format take precedence;
--schema-cmd= disables the configured source. --env selects an
environment-scoped block. Empty command output is rejected. PATH and PWD
cannot be overridden through external_schema.env; use an explicit executable
path and working_dir.
Native ptah runs the configured block only with --allow-config-exec; see
Programs a project file names. An explicit
--schema-cmd names the program itself and needs no further permission.
Relative working_dir values are constrained to the current working directory
after symlink resolution. Ptah bounds output, redacts secrets and terminal
control characters from stderr and parser diagnostics, and cleans up descendant
processes.
Continue with Atlas project config for the supported
atlas.hcl subset.
Programs a project file names
Section titled “Programs a project file names”A project file can name a program that Ptah would start on your machine:
- an
atlas.hcldata "external"ordata "external_schema", including one that adata "composite_schema"part or alocalreaches; - a
ptah.yamlexternal_schemablock; - a
ptah.yamlmigration.pre_up_hookormigration.pre_down_hook, which runs through the shell; - an
externalstep in a.test.hclcase.
The two binaries treat these differently.
ptah-compat runs them as Atlas does. A project that switched from Atlas
keeps its atlas.hcl working without a Ptah variable. Every local is
evaluated, as Atlas evaluates it, so a data "external" that a local reads
runs whichever env is selected. A data source that nothing reads never runs.
PTAH_ATLAS_STRICT_COMPAT=1 keeps the community binary’s behavior: it refuses
data "external_schema" with data.external_schema is not supported by the community version of Atlas and runs data "external" and external steps.
Native ptah refuses them unless you pass --allow-config-exec, or set
PTAH_ALLOW_CONFIG_EXEC=1. Reading a project file is not permission to run
code from it. The refusal names the construct and the executable, for example
atlas.hcl data "external" "probe" runs sh: a program named in a project file does not run without permission; pass --allow-config-exec to run it. Native
ptah reads atlas.hcl lazily: it evaluates only what the selected env, the
top-level attributes and the diff, lint and exporter blocks reach. A
program that only another env needs does not run, even with the permission.
A program you name on the command line, --schema-cmd, --pre-up-hook or
--pre-down-hook and their environment twins, is your own command and runs
without the permission. The older spellings --allow-external-schema and
PTAH_ALLOW_EXTERNAL_SCHEMA grant the same permission as
--allow-config-exec on the commands that register them.
PTAH_ALLOW_EXTERNAL_TEST_COMMAND grants it for external steps only.
The permission is checked twice with one policy: while sources are classified, before a database is contacted, and again where the program starts. A run that reaches a program by a path that skipped classification is refused at the start.
Upgrading from 0.15. Native ptah 0.15 and earlier ran a ptah.yaml
migration.pre_up_hook or migration.pre_down_hook, and an atlas.hcl
data "external", without asking. From 0.16.0 they are refused until you pass
--allow-config-exec or set PTAH_ALLOW_CONFIG_EXEC=1. A deploy job whose
ptah.yaml names a hook, often a backup, now stops before it connects to the
database, with nothing applied and the hook not run, until the job adds one of
the two. --allow-external-schema does not cover it: migrations up and
migrations down do not register that flag, and PTAH_ALLOW_EXTERNAL_SCHEMA
is read only by the commands that do. The same hook passed as --pre-up-hook
or PTAH_PRE_UP_HOOK runs without the permission, as before. ptah-compat is
unchanged.
Permission is not a sandbox. An allowed program runs with your user’s permissions. Ptah runs it without a shell (except a hook, which is shell by definition), with an explicit argument vector, a timeout, bounded output, redacted secrets in what it reports, and cleanup of the process tree.
Two other things a project file selects are deliberately outside this policy.
A docker:// dev URL or an atlas.hcl docker block starts a container image
on your Docker daemon, with no host mounts or privileges; review the image
name as you would in a compose file. A provider manifest is read only from a
path you pass with --provider-config or PTAH_PROVIDER_CONFIG, never
discovered, and each executable in it is checked against the digest the
manifest holds.