Command Reference
Interactive Mode
Launches interactive mode with a menu of all available actions. Best for day-to-day use: guides you through each step with prompts and confirmations.
Configuration
Every command reads project settings from hetge.jsonc in your project root.
The $schema line gives editors autocomplete and inline validation. It is the
only config location: a package.json / pyproject.toml config from
before 1.0 is moved once with npx @hetge/cli@0.22 config:migrate, which also
folds in .env.deploy.* files and then deletes them (run it on a clean git tree;
it reads each file, never its .tpl, so a refusal prints the op inject
line that creates a file from its template first), and a hetge.json is renamed
to hetge.jsonc.
{
"$schema": "https://cli.hetge.com/hetge.schema.json",
"onepassword": {
"items": [
{ "file": ".env", "item": "my-project" }
]
}
}
Environment Commands
Downloads .env files from 1Password using your .tpl template files.
Templates define which secrets to fetch. Name one configured file to pull just that one
(hetge env:pull .env.staging). Use --force to overwrite existing local files without prompting.
A no at the overwrite question (or no terminal to ask) writes nothing for that file, and the other
configured files are still pulled; Ctrl-C or Escape stops the whole run there. Either way it exits
130.
Uploads local .env files back to 1Password.
Syncs any changes you've made locally to the vault. Name one configured file to push just that one
(hetge env:push .env.staging). Use --force to overwrite vault items without prompting.
A no at the update question (or no terminal to ask) writes nothing for that file, and the other
configured files are still pushed; Ctrl-C or Escape stops the whole run there. Either way it exits
130.
An empty variable (KEY=) is kept: 1Password holds no empty field, so the template carries it
as KEY= and env:pull writes it back empty. Empty and missing are not the same
where one env file overrides another.
Reports whether an env file, 1Password and Dokploy still agree.
Three comparisons per target: the file against its .tpl, what a sync
would upload against what Dokploy holds, and the template resolved through op
against the local file. Every deployable target, or --target just one. It writes
nothing and prints variable names only, never a value, and exits 2 when anything is
out of step, so it works in a script. It exits 1 when a comparison it asked for
failed, even beside drift: op inject erroring or timing out, Dokploy not answering,
an id the server no longer has, a missing docker-compose.yml, and a synced target
with no operator credentials, so a CI job missing its Dokploy secrets fails rather than passing
unchecked. An edge that does not apply (no .tpl, a local-only target, never
synced, the dokploy feature off) is named as not checked and is not an error.
--json prints the same reports as one JSON document on stdout:
clean is true when no edge failed and none drifted, complete is false
when one failed, compared counts the edges actually compared, and an edge that
compared nothing is {"unchecked": reason, "failed": true|false}. With nothing to
compare at all, clean is true and compared is 0. This is the one command that always asks 1Password, so
op not being signed in, or op whoami timing out, is an error here rather than a skipped line. In a terminal
it runs op signin first, without asking; unattended it cannot, since
op signin asks for a password or a fingerprint, so sign in before a script runs it.
An op whoami that does not answer in time is not a missing session (1Password is
usually waiting on an unlock): it says 1Password did not answer, exits 1, and runs
no op signin.
Runs a local command with a deploy target's environment variables set, for example
hetge exec --target staging -- pnpm prisma migrate deploy.
Everything after -- is passed through verbatim, the working directory is the
project root, and the command's exit code is propagated. Without the --
nothing runs: it prints the line with the -- in front of the program, whose
words (flags such as -v or --target included) stay the program's.
Schema-rewriting commands (migrate dev, migrate reset,
db push) are refused against a target that describes a remote environment.
Local Development
Starts one shared Postgres 18 / Redis 8 / RustFS stack for every app on the
machine, then provisions this project from its dev env file (dev.env, default
.env.development.local): the POSTGRES_* trio becomes a role and a
database, every S3_*BUCKET* variable a bucket. A file pointing anywhere but the
local stack (a tunnel port, a production S3 URL) is refused, naming the variable. Works outside
a project too, where it only starts the stack.
Whether the stack is running, and whether this project's database and buckets exist.
Replaces this project's local database with a copy of staging's: pg_dump streams
from the staging container over the ssh alias (dev.pull) straight into
the local database. No tunnel, no local Postgres tools, no password over the wire. Asks first;
--yes answers.
Drops and recreates this project's local database and buckets. Other apps' data is untouched.
Stops the stack and keeps the data. --volumes also deletes the stack's volumes, which is every app's local data, so it asks first.
Build & Deploy Commands
Builds a Docker image and pushes it to GitHub Container Registry (GHCR).
Pushes latest plus, when git information is available, an immutable
sha-… (or dev-… for uncommitted changes) tag. No build ever
writes a version tag: that is hetge release's job, onto an image that already exists.
Add --deploy to automatically trigger deployment after a successful build.
Requires a running Docker daemon (with buildx) and a GitHub login
with write:packages; both are checked automatically, and the CLI will prompt for
gh auth login if needed.
--target picks a deploy target declared in hetge.jsonc; without it
the configured default applies.
With --deploy and no --sync, a build whose target is out of
step with Dokploy is offered the fix first (Dokploy (production) is out of step. Sync it
now, before the build?); yes syncs and carries on, no just prints the command.
--sync uploads the compose, env and domain config before deploying, and
--no-cache rebuilds without the Docker layer cache.
Builds, pushes and deploys to the staging target, whatever
deploy.default says. This is the first rung of the ship workflow:
hetge release names the image staging runs, so production ends up on the exact
bytes that were tested. A dirty tree is allowed, because that is what staging is for.
--sync uploads the compose, env and domain config before deploying;
--no-cache rebuilds without the Docker layer cache. An app with no staging
target is refused before anything else runs, naming hetge build --deploy as its
equivalent.
When staging's Dokploy config is out of step, it offers to sync before building
(Dokploy (staging) is out of step. Sync it now, before the build?) rather than only
printing the command; --yes answers yes, so a scripted stage syncs first.
With --sync the offer is not made, since that run syncs anyway.
Triggers a deployment on Dokploy using the existing image on GHCR. No rebuild occurs: this just deploys what's already been pushed. --target chooses which deploy target is deployed. When the target's Dokploy config is out of step, it offers to sync first (Dokploy (staging) is out of step. Sync it now, before the deploy?); declining deploys against the config Dokploy already has, as before.
Creates or updates your project configuration on Dokploy.
Sets up the project, environment, compose file, and domain.
Run this first when setting up a new project.
Add --deploy to trigger deployment after syncing.
--target uploads that target's env file, appends the managed
IMAGE_TAG / CONTAINER_NAME block, and creates or reuses the
target's own Dokploy environment.
On a Dokploy application (dokploy.application), it writes
the name, description, env and domains, keeps the build args and secrets as they are, and
never creates the application.
When Dokploy holds variables the upload does not, it stops and names them
(Dokploy has 2 variables local does not: W, Z. Uploading removes them. Continue?)
before anything is sent. The default is no; --yes answers yes, and a run with
no terminal and no --yes cancels (exit 130). That is usually somebody's edit in the Dokploy
UI, which the upload would otherwise delete without a word. The names are printed even under
--yes. Two uploads are refused outright, because they look like a wrong or empty
file rather than an edit: one that would remove variables and leave the app none, and one removing
more than half of the app variables Dokploy holds (from two removals up; on a compose, hetge's own
IMAGE_TAG, CONTAINER_NAME and HETGE_VERSION count on
neither side, though a stale one is still named; an application has no such names, and all of
its variables count).
An empty upload that removes nothing goes ahead. --allow-env-removals is the
deliberate way through, on sync, deploy, stage and
build.
--dry-run prints what a sync would change (the env names it would add, change
or remove, the compose file, the name and description, the domains, what it would create and the
id it would record) and writes nothing, not even the recorded id. It makes no offers on the way,
and exits 1 when the sync itself would refuse or stop on an error.
A Dokploy that cannot be reached is offered a retry at a terminal only; --yes
never answers it.
With a staging target it names the image staging already runs, and builds no main image and
deploys nothing. On an app with no staging target it releases HEAD, and deploys production when
hetge deploys the app: it reuses the image production runs when that is HEAD's build, and
otherwise, on production's latest, builds HEAD first.
Bumps the version in package.json (or pyproject.toml),
commits it as v1.2.7, writes release notes from the commits since the last tag,
tags that commit, pushes, creates a GitHub Release, and only then points the
1.2.7 and released tags at the staged digest, so
released never names a digest whose release does not exist.
Refuses, reporting every problem at once before writing anything, on a dirty tree, off the
release branch, when gh is not authenticated (only for a run that creates a GitHub
Release), and on the
staged image itself: not in the registry (run hetge stage first), not built from
HEAD, built from a dirty tree, already released, or nothing to release because HEAD is the
last version tag. A failure from the push on is never rolled back: it prints what is
still owed (the push, gh release create, the registry tags, and the production
deploy on an app with no staging target), since a second release refuses once the tag exists. An app that is not promote-clean also gets a
<version>-production flavour, built from HEAD before the bump.
--no-github-release skips the GitHub Release for that run.
Before any of that it checks the environment behind the image being named, against its
.tpl, against Dokploy and against 1Password, and refuses when they disagree:
Refusing to release: the staging env is not in step (.env.staging). The drifted
variables are grouped by what fixes them, under Stage again (baked into the
image as build args, so only a rebuild helps; Build again and
hetge build --deploy on an app with no staging target) or
Push and sync, each with its commands. --allow-drift goes ahead anyway and prints one line naming what it
overrode; it is command mode only, since the interactive menu cannot pass a flag, so a
release refused from the menu returns to the menu. When 1Password has no session, a terminal
run offers to create one rather than skipping that edge; unattended, and under
--yes, the offer is not made and the vault edge is skipped as before.
Ships the released image to production: nothing is rebuilt, so production
receives the exact digest staging ran. It refuses, listing every failed check at once, when no
staging target is declared, when nothing has been released (run hetge release first, or
--allow-unreleased to ship what staging runs), when the app is not promote-clean and no
released-production flavour exists, when that flavour was built from a different
commit or a dirty tree, when stable already points at that digest, or when the
production target does not pin stable. A registry that does not answer is said
to be unreadable, never taken for nothing released or no stable yet. On agreement
it points stable-previous at whatever stable was, moves
stable to the promoted digest, and deploys production. If that deploy fails,
stable stays moved and the error names hetge deploy --target
production to finish. --allow-unreleased means ship what
the staging target runs, bypassing released, and says so loudly.
Production's environment is checked first, the same three ways hetge release
checks staging's, and a refusal there is reported on its own before the promotion checks are
even reached. Like release, a terminal run with no 1Password session offers to
create one rather than skipping the vault edge; unattended, and under --yes,
that edge is skipped as before. --allow-drift lifts that one refusal and
nothing else, and only from the command line: the interactive menu cannot pass it, so a
promote refused there returns to the menu.
Swaps stable and stable-previous, then deploys production, so a bad
promote is one command away from undone and the rollback is itself reversible.
It never touches released or a version tag.
--tag names a different image to become the new stable, keeping the
current one as previous. Refuses a rollback that would not change what runs, naming
hetge deploy --target production for when stable is right but
production is not. If the deploy fails after the tags moved, it says so and names that same
deploy: do not run rollback again then, since it would swap the tags back.
One row per deployable target: version, commit, branch, clean or dirty, released or not,
build time, and (promoted) for the target running stable when its
image could be read. Between
staging and production sits a released row saying nothing released yet: hetge
release, production behind: hetge promote or production runs it (on a
production-only app, production runs unreleased work: hetge release, since only a later
build --deploy moves production off what its release deployed). Then the
local HEAD, marked (release commit of v1.2.7) when HEAD is a release's
bump commit, and a not promote-clean line when the staging image's build args
differ between the targets' env files. The version comes from the tags at the image's digest,
the bare semver tag hetge release wrote, never from a label, and a
1.2.7-production flavour reads as the release it was built for, so production on
it runs the release; when gh cannot list the package's tags it reads
- and one line says why.
Last comes an env line about the files rather than the images: per target, whether
the env file is in step with its template and with what Dokploy holds, with the command that
puts it right. 1Password is not consulted here, because status only reads;
hetge env:check is where that question is asked. --json prints
the same facts as one JSON document on stdout: each image with its branch and whether it is
the checked-out commit (atHead), the released row's verdict,
releasedAhead on production when there is something to promote, the release commit, the build args that are not promote-clean, and the env reports.
A registry that does not answer is not an image that was never pushed: the row says
could not read (readError in the JSON) and the command exits 1
after printing the rest.
status for every project below the current directory: every hetge.jsonc
one or two levels down, one row per deployable target, with version, image state, released,
synced (checked against the server when it can be reached), env drift against Dokploy, the last
deploy's outcome, and whether the image is the checked-out HEAD. Run it from the directory that
holds the checkouts, not inside one. --json prints the rows, with the reasons
behind each cell. An image the registry could not be read for is unreadable, never
missing, and the command exits 1 after printing the rest. A Dokploy
application reads by dokploy: Dokploy builds it from git, so there is no image to judge.
A checkout that does not load, one still configured in a pre-1.0 location included, is named
under the table rather than left out.
Every project, environment and service on the Dokploy server, with its status, last deploy and
domains. It reads the server rather than a project, so it works with no checkout; inside one it
uses that project's operator profile, and says so when that project's config does not load and
the shared credentials are read instead. --profile reads another organization's
credentials (~/.hetge/env.<name>). --json prints
{ host, profile, rows }.
Setup & CLI
Writes a hetge.jsonc through a short wizard: the vault and env files, which
features are on, and the Dokploy project and service. The file goes beside the nearest
package.json or pyproject.toml at or above the current directory, or
at the repository root when there is none. Discovery stops at the git repository root, so a
config above the repository is never adopted. It refuses wherever a config already exists,
including one that does not load (a pre-1.0 location or an invalid hetge.jsonc),
and prints the reason instead of writing over it.
Refreshes GitHub authentication (with write:packages) and, when
Docker is installed and running, forces a fresh docker login ghcr.io using the
current gh token, whatever the project's features (without Docker it says so and skips that
step). It works in any directory. Use this if your token expired or you switched GitHub
accounts mid-session.
It also pulls the shared operator credentials (the Dokploy URL and API key) into
~/.hetge/env, and the GitHub Packages token into ~/.npmrc, both
readable by you alone. Inside a project with an operator profile it fills that profile's
~/.hetge/env.<profile> instead; --profile fills
~/.hetge/env.<name> from anywhere, which is the file
server:status --profile <name> reads.
Updates hetge to the latest version, with the package manager that installed it. When it cannot ask GitHub which version is the latest (gh missing or logged out, or no answer within a minute), it says so, names the command to run by hand, and exits 1, rather than calling the installed version the latest.
Prints the version and its release date.
Lists every command with its flags and what each does. The same list drives argument
checking, the interactive menu and shell completion, so it cannot fall behind.
hetge help build (or hetge build --help) shows that one command:
its usage, the values it takes and every flag with its description. A name that is not a
command is refused, with the closest one suggested.
Prints a guide condensed for the terminal: ship-it (stage, release, promote) or
run-it (the local stack). No topic means a picker, or the list when piped.
Prints a shell completion script, generated from the same command list as this page.
--install writes it to the shell's standard location. With no shell named,
a terminal gets a guided setup; without one, it says how to name a shell and exits 130.
Typical Workflow
Notes
0 done (for a check, in step), 1 an
error, 2 drift (env:check found some, or release /
promote refused because of it), 130 cancelled: Ctrl-C or Escape at a
prompt, or a "no" to a confirmation that ends the command (with no terminal and no
--yes, nobody can say yes, so it is a "no"; so do hetge init, bare
hetge and hetge completion with no shell, which need a terminal to ask
at all). A run stopped by a signal exits 128 plus its number, as a shell reports it:
130 for Ctrl-C, 143 for SIGTERM. hetge exec exits with its
command's own code instead. Under --json stdout is the JSON and nothing else:
warnings and errors go to stderr, and nothing asks a question, not even op signin
(env:check --json without a session exits 1). A minor release may add JSON
fields; removing or renaming one is a major release. A command given an argument it does not take is refused
rather than ignored, and so is a switch given a value (--deploy=false: leave the
flag out instead), an empty value (--tag=), or a flag given two different values
(--target staging --target production)
hetge.jsonc; the Dokploy URL and API key live in ~/.hetge/env, shared by every project (or in ~/.hetge/env.<profile> for a project that names an operator profile), and hetge login fills them
stage → release → promote;
Run It Locally covers the shared dev stack.
Both are condensed in the terminal too: hetge guide ship-it and
hetge guide run-it
.tpl templates are updated, run env:pull to fetch the latest secrets
.env files locally, run env:push to sync to 1Password, then sync to update Dokploy
docker-compose.yml or a target's domains change, run sync to update Dokploy. Sync deletes any domain the config does not declare, so read the plan it prints
dokploy.application instead of
dokploy.compose, with features.build off: sync manages its env and
domains, deploy rebuilds it from git, and no GitHub login is needed
build. The CLI verifies
the daemon on every build and prints platform-specific hints (e.g. open -a Docker on
macOS) if it's missing or stopped