README
¶
devvault
[!WARNING] Work in progress — devvault is under active development and not yet ready for production use. Commands, configuration formats, and behavior may change without notice between releases.
devvault is a macOS-first CLI for reproducible, isolated Terraform development environments.
It prefers Apple container on supported Apple Silicon macOS 26 hosts and falls back to Docker Desktop with Docker Compose v2.
Contents
- Requirements
- Installation
- Quick start
- Command overview
- Flags that apply almost everywhere
- Registered projects
- Setting up a project
- Selecting and inspecting projects
- Health checks and repairs
- Checking the registry itself
- Moving to another machine
- Credentials
- Working in the container
- The Terraform lifecycle
- Security and quality
- Customer-wide workflows
- Editor integration
- Shell integration
- Terraform or OpenTofu
- Trusted project files
- Keeping the pinned tool versions current
- Updating devvault
- Exit codes
- Network access
- Configuration
- Support
Requirements
- macOS, with Apple Silicon recommended for Apple
container. - Apple
containeron supported macOS 26 hosts, or Docker Desktop with Docker Compose v2. - Git.
- Xcode Command Line Tools.
- AWS CLI when using AWS credential checks and Terraform workflows.
cosign, optional, to verify a release signature and to letdevvault updateverify one.- Go 1.25 or newer only if you build devvault from source. The released binary has no Go dependency.
Installation
Download the archive for your Mac from the latest GitHub release, together with checksums.txt and checksums.txt.sigstore.json:
- Apple Silicon:
devvault_Darwin_arm64.tar.gz - Intel:
devvault_Darwin_x86_64.tar.gz
Verify the release before you run it. Step 1 proves who built it and needs a current cosign (--bundle is not in old releases); step 2 proves your download matches what was signed:
cosign verify-blob checksums.txt \
--bundle checksums.txt.sigstore.json \
--certificate-identity-regexp 'https://github\.com/Marcel2409Dev/devvault-cli/\.github/workflows/release\.yml@refs/tags/v.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
shasum -a 256 -c checksums.txt --ignore-missing
Releases up to 0.7.0 carry the same signature under the name checksums.txt.bundle; pass that name to --bundle when you verify one of them. Both names are published for now, and checksums.txt.bundle goes away in 0.10.0. devvault update handles this itself: it reads checksums.txt.sigstore.json and falls back to checksums.txt.bundle.
If either step fails, do not run the binary. Then unpack and install:
tar -xzf devvault_Darwin_arm64.tar.gz
mkdir -p ~/.local/bin
install -m 755 devvault ~/.local/bin/devvault # make sure ~/.local/bin is on your PATH
devvault version
devvault --help
Use the x86_64 archive name on Intel Macs. Any directory on your PATH works, including /opt/homebrew/bin: devvault update replaces the running binary in place and only refuses when that binary is actually managed by Homebrew — that is, when it resolves into a Cellar or Caskroom directory — because replacing it there would leave brew with a binary it did not install.
Each archive also contains shell completions under completions/ (bash, zsh, fish) and man pages under man/; install them wherever your shell and man look. Every archive ships an SBOM (*.sbom.json) next to it in the release. The pages under man/ are produced by the hidden devvault man --dir <directory> command, which exists for packaging and is not part of the everyday surface.
Releases are not Apple-notarized yet, so Gatekeeper quarantines the downloaded binary. Verify the signature first, then clear the attribute with xattr -dr com.apple.quarantine <path>.
Homebrew is the simpler route: every release publishes a cask to the tap
Marcel2409Dev/homebrew-tap, which installs the binary, the man pages and the
shell completions in one step.
brew tap Marcel2409Dev/tap
brew install --cask devvault
To update, use brew upgrade devvault for a Homebrew installation, and
devvault update for one you installed by hand — devvault
refuses to replace a binary Homebrew manages, so the two never fight over the
same file. To uninstall, brew uninstall --cask devvault or remove the binary
you installed.
Quick start
Start devvault and navigate the guided menu:
devvault
The menu groups the common workflows into Projects, Setup, Terraform, Credentials, and System. You can also open it explicitly:
devvault menu
Running devvault without arguments opens that menu only when it has a terminal and a project is already active. Without an active project it starts devvault quickstart instead, and without a terminal it prints the help.
Direct commands stay available for automation, wrappers, and fast shell workflows:
devvault init # generate the devvault files
devvault add . --id kunde-a/network_p # register the project — required
devvault doctor
devvault build
devvault tf version
devvault validate
devvault ij
devvault init deliberately does not register. Registering is where you state
which customer and credential set a directory belongs to, and a repository must not
make that statement about itself. Until a project is registered, devvault mounts no
credentials for it and refuses to run anything in its container — see
Registered projects. devvault clone, devvault setup and
devvault quickstart register on their own.
If the id you pick names a different customer or credential set than the config
init generated, run devvault sync afterwards so the generated files match.
devvault doctor exits with status 1 when a required host check fails — architecture, git, and "at least one container backend is available". Everything else is reported as a warning. --fix performs the repairs devvault can make safely and locally; it never installs software, starts a daemon, or touches the network.
Clone and prepare a customer repository:
devvault clone git@github.com:example/kunde-a-platform.git
devvault clone git@gitlab.com:example/kunde-a-platform.git
devvault clone git@bitbucket.org:example/kunde-a-platform.git
devvault open git@github.com:example/kunde-a-platform.git
Manage registered projects:
devvault add /path/to/repo --id kunde-a/platform --alias kunde-a
devvault list
devvault use kunde-a
devvault current
devvault tf version
cd "$(devvault cd kunde-a)"
Set up a customer with credential sets:
devvault setup customer \
--customer kunde-a \
--repo /path/to/network-p \
--project network_p \
--credential-set network_p \
--profile network-p-dev \
--auth existing
devvault setup project \
--customer kunde-a \
--repo /path/to/network-c \
--project network_c \
--credential-set network_c \
--profile network-c-dev \
--auth credential_process \
--credential-process "/usr/local/bin/customer-auth network-c"
Credentials are grouped per customer and set:
~/.aws-kunden/kunde-a/network_p/config
~/.aws-kunden/kunde-a/network_c/config
Check or log in:
devvault credentials list kunde-a
devvault credentials status --project kunde-a/network_c
devvault credentials login --project kunde-a/network_p
Git metadata is detected for GitHub, GitLab, Bitbucket, self-hosted Git servers, and local bare repositories. devvault list shows projects grouped by customer with their credential set and Git provider.
Generated wrapper scripts under .devvault/scripts/ are intended for configured editor, Codex App, and shell usage.
New projects keep DevVault-owned files under .devvault/, including config.yaml, docker-compose.yml, Dockerfile, and wrapper scripts. Standard integration files such as .devcontainer/devcontainer.json, GitHub Actions, GitLab CI, and pre-commit configuration stay in their conventional locations.
Command overview
One line per command. The sections below describe each of them in context.
| Command | What it does |
|---|---|
devvault |
Opens the interactive menu, or starts quickstart when no project is active |
add |
Registers an existing devvault repository in your registry |
apply |
Runs terraform apply in the container; needs --yes |
apply-plan |
Applies a plan saved with plan --save; needs --yes and refuses a plan the code has moved past |
backend doctor |
Runs terraform init -backend=false to check backend configuration |
build |
Builds the project's development container image |
cache |
Shows the shared Terraform provider caches and reclaims their disk space: list, prune |
cd |
Prints a registered project's path, for cd "$(devvault cd …)" |
clone |
Clones a Git repository, generates the devvault files and registers it |
completion |
Generates the shell completion script for bash, zsh, fish or PowerShell |
context |
Prints customer, project, credential set, AWS profile, Terraform version and backend |
credentials |
Creates, inspects and refreshes the AWS config of a credential set |
current |
Prints the active project |
customer |
Runs doctor, validate, scan or docs across all projects of one customer |
destroy |
Runs terraform destroy in the container; needs --yes |
docs |
Runs terraform-docs and injects the result into README.md |
doctor |
Checks host and backend readiness; --fix applies the safe local repairs |
fix |
Recreates missing local support directories and, with --regenerate, missing generated files |
graph |
Renders terraform graph into terraform-graph.svg with Graphviz |
ij |
Switches between open IntelliJ projects |
images |
Shows the container images devvault built and reclaims their disk space: list, prune |
import |
Runs terraform import in the container; needs --yes |
init |
Generates the devvault files in the current repository, without registering it |
lint |
Runs tflint and the enabled filesystem scanners |
list |
Lists registered projects, grouped by customer |
menu |
Opens the interactive menu explicitly |
modules |
Module helpers: doctor validates recursively, changelog prints guidance |
open |
Opens a project in the configured editor, cloning a Git URL first if needed |
overview |
Shows every registered project in one table — image, trust, pins, credentials — with the one command each needs |
plan |
Runs terraform plan in the container; --save writes a protected plan file |
policy check |
Runs conftest test . |
precommit |
Installs or runs the pre-commit hooks inside the container |
quickstart |
Interactive first-project setup |
registry check |
Reports registrations that no longer describe reality, with the command that fixes each one |
registry export |
Writes the project mapping as a portable document, without the records that only hold on this machine |
registry import |
Restores those registrations on another machine, for every checkout it finds |
remove |
Removes a project from the registry, leaving the files alone |
scan |
Runs the enabled security scanners over the project |
secrets scan |
Runs a gitleaks secret scan |
setup |
Guided customer/project setup: setup customer, setup project |
shell |
Opens an interactive shell in the development container |
state |
Terraform state helpers: list, pull, mv, rm |
status |
Reports project readiness with per-check hints |
sync |
Regenerates the devvault-managed files from your trusted config |
tf |
Runs an arbitrary Terraform command in the container |
tools |
Compares the pinned tool versions against upstream: outdated, bump |
trust |
Inspects and approves the devvault files a repository ships: status, review, accept |
update |
Replaces this devvault binary with the latest release |
use |
Sets the active project |
validate |
Runs the fmt / init / validate / tflint chain |
version |
Prints the devvault version and build information |
workspace |
Terraform workspace helpers: list, show, select |
Flags that apply almost everywhere
-p, --project <id-or-alias> selects the project a command acts on. It is
accepted by every command that touches a project: apply, apply-plan,
backend doctor, build, cache prune, context, credentials login,
credentials status, destroy, docs, fix, graph, images prune,
import, lint,
modules doctor, plan, policy check, precommit install, precommit run,
scan, secrets scan, shell, state …, status, sync, tf, tools bump,
tools outdated, trust accept, trust review, trust status, validate and
workspace ….
The value may be the full id <customer>/<project>, the alias, the project name,
or the base name of the project directory; a value that matches more than one
project is rejected as ambiguous instead of guessed. Without -p, devvault uses
the current directory when it contains .devvault/config.yaml (or the legacy
.devvault.yaml), and otherwise the active project set by devvault use.
--json prints one machine-readable document on stdout. It exists on
cache list, cache prune, context, credentials list, credentials status,
current, doctor, fix, images list, images prune, list, overview,
registry check, registry export, status, sync,
tools outdated, trust status and version. On registry export it selects
the format of the exported document rather than a report about it.
credentials status --json moves the AWS CLI's own
output to stderr so stdout carries exactly the one document devvault promises,
and cache prune --json / images prune --json refuse to run without --yes
rather than draw a confirmation prompt over that document.
--yes confirms an operation devvault will not perform on its own. It has
three distinct roles:
- On
apply,apply-plan,destroy,import,state mvandstate rmit is a hard gate: without it the command stops with… is destructive or mutating; rerun with --yes to confirm, before anything is started. These commands also print customer, workspace, AWS profile and credential set before they run, so you can see which account you are about to change. Onapply-plan,--yesconfirms that the run changes infrastructure and nothing else — that the saved plan still matches the code is a separate question with a separate flag, see apply, apply-plan, destroy, import. - On
add,cache prune,images prune,trust accept,tools bumpandupdateit replaces an interactive confirmation. Without a terminal those commands refuse rather than assume consent; the reasons are in Registered projects, The shared provider cache, The images devvault builds, Trusted project files, Keeping the pinned tool versions current and Updating devvault. - On
registry importit says that an existing registration may be replaced by the one in the document. Without it such a project is reported and skipped; see Moving to another machine.
Those are the only commands that have --yes. The read-only passthrough
subcommands — state list, state pull, workspace list, workspace show,
workspace select and secrets scan — change nothing, so they neither
document the flag nor consume it: whatever you pass them goes to the tool in
the container, and an unknown flag fails there rather than being silently
swallowed.
Passthrough commands. tf, plan, apply, destroy, import,
workspace …, state … and secrets scan forward their arguments to the tool
in the container. devvault only interprets the flags in the leading run of
flags: --help there is devvault's own help, a -- there ends devvault's
interpretation and is removed, and everything from the first non-flag argument
on belongs to Terraform. That also covers devvault's own flags on plan, so
-- is the way to pass a Terraform flag whose name devvault happens to use too.
devvault plan --help # devvault's help for plan
devvault tf plan --help # Terraform's help for plan
devvault tf -- -help # reaches Terraform as: terraform -help
devvault plan -- --save # reaches Terraform as: terraform plan --save
devvault plan --save -- -target=module.network # --save is devvault's, the rest is not
devvault tf -p kunde-a state list
devvault tf -p=kunde-a state list
All four spellings of the project flag work here — -p <value>, -p=<value>,
--project <value> and --project=<value>. devvault removes it before the rest
is forwarded, so it never reaches Terraform.
Registered projects
devvault mounts AWS credentials into a container that then runs the repository's own
code, so which host directory that is can never be the repository's decision. It comes
from your own registry under ~/.devvault, which is written by devvault add,
devvault clone, devvault setup, devvault quickstart and
devvault registry import — and by nothing inside a clone.
A project that is not registered therefore gets no credentials at all, and every command that runs something inside the container refuses before a backend is even selected. Registering is what unblocks it:
devvault add . --id kunde-a/network_p
Without --id, devvault add falls back to the customer, project and credential set
the repository's own config proposes. That proposal decides which host directory gets
mounted, so devvault prints it and asks for confirmation first; without a terminal it
refuses and asks for --id or --yes.
Where the registry names no mount — because the credential set has none, or because
there is no credential set for the project at all — devvault derives it from the
registered customer and credential set, ~/.aws-kunden/<customer>/<set>. That is a
registry decision too: the repository's aws.credentials_mount never steps in for a
missing one. It is also why devvault registry export can leave a mount behind that
holds only on the old machine — see
Moving to another machine.
Read-only commands keep working and report the missing registration honestly rather
than naming a path: devvault context prints (not registered) for the AWS profile
and config path, devvault status lists the credentials mount as missing, and
devvault list, devvault credentials list and devvault trust status are
unaffected. devvault credentials status and devvault credentials login do refuse —
they would have to point the AWS CLI at a config file the repository picked.
Setting up a project
There are four ways into a devvault project. They differ in how much they decide for you, not in what they produce.
devvault quickstart
Interactive, and the right choice for a first project. It asks whether to initialize the current directory, use an existing local repository, or clone a Git URL, then asks for customer id, project name, credential set and AWS profile, generates the files, registers the result and prints the readiness status.
devvault quickstart
It requires a terminal; in a script it stops with a message pointing at
devvault setup and devvault clone.
devvault setup
The scriptable variant of the same thing. devvault setup customer creates a new
customer together with its first project, devvault setup project adds a project
to an existing customer, and plain devvault setup behaves like
setup customer. All three take the same flags; missing values are asked for
interactively when devvault has a terminal, so a fully flagged invocation is
non-interactive.
devvault setup customer \
--customer kunde-a \
--customer-name "Kunde A GmbH" \
--repo git@github.com:example/kunde-a-platform.git \
--project network_p \
--credential-set network_p \
--profile network-p-dev \
--auth sso \
--sso-start-url https://kunde-a.awsapps.com/start \
--sso-region eu-central-1 \
--sso-account-id 123456789012 \
--sso-role-name AdministratorAccess
| Flag | Meaning |
|---|---|
--customer |
Customer id, e.g. kunde-a |
--customer-name |
Customer display name |
--workspace |
Customer workspace directory |
--repo |
Git URL or local repository path |
--project |
Project name, e.g. network_p |
--credential-set |
Credential set name |
--profile |
AWS profile name |
--auth |
sso, assume_role, credential_process or existing |
--distribution |
terraform or opentofu; detected from the repository when omitted |
--terraform-version |
Explicit version of the selected distribution |
--region |
Default AWS region, default eu-central-1 |
--sso-start-url, --sso-region, --sso-account-id, --sso-role-name |
AWS SSO parameters |
--role-arn, --source-profile |
assume_role parameters |
--credential-process |
credential_process command |
--force |
Overwrite generated files and config |
setup prints the resulting project id, path, distribution, Terraform version
and credentials mount.
devvault clone
Clones a Git repository into ~/DevVault/<repo> (override with --base-dir, or
pass an explicit target directory), generates the devvault files and registers
the result.
devvault clone git@github.com:example/kunde-a-platform.git
devvault clone git@github.com:example/kunde-a-platform.git ~/work/platform
devvault clone git@github.com:example/kunde-a-platform.git --id kunde-a/platform --alias platform --open
| Flag | Meaning |
|---|---|
--base-dir |
Base directory for cloned repositories, default ~/DevVault |
--id |
Project id, e.g. kunde/platform |
--alias |
Short project alias |
--no-init |
Clone only, do not generate the devvault files |
--force-init |
Overwrite generated devvault files after cloning |
--open |
Open the clone in the configured editor |
--editor |
Editor override: intellij, vscode, cursor, codex, custom |
--allow-custom-editor |
See Editor integration |
An existing target directory is not overwritten; the clone step is skipped and
reported as clone: skipped, directory already exists.
devvault init and devvault add
init generates the devvault files in the current repository and nothing else.
It is the right entry point for a repository you already have checked out.
devvault init
devvault init --force --readme
devvault init --no-create-credentials-dir
| Flag | Meaning |
|---|---|
-f, --force |
Overwrite generated files |
--readme |
Also generate README-devvault.md |
--no-create-credentials-dir |
Do not create the configured AWS credentials mount directory |
The files it writes are .devvault/config.yaml, .devvault/Dockerfile,
.devvault/docker-compose.yml, the wrapper scripts .devvault/scripts/tf,
validate, lint, docs, plan, scan and context, plus
.devcontainer/devcontainer.json, .pre-commit-config.yaml,
.github/workflows/terraform.yml and .gitlab-ci.yml.
add registers a directory. This is the step that makes the project usable —
see Registered projects for why it cannot be skipped and
why it asks before accepting an identity the repository proposed.
devvault add . --id kunde-a/network_p
devvault add /path/to/repo --id kunde-a/platform --alias kunde-a
devvault add /path/to/repo --yes # accept the repository's proposal
add prints the credentials mount it registered, and warns when the generated
files no longer match the resulting configuration — the fix is devvault sync -p <id>.
It also reads the repository's origin remote and records the same Git metadata
devvault clone stores: provider, host, owner and repository name. Those fields
are descriptive only — nothing about them decides what devvault mounts or runs —
but two commands use them: devvault list shows the provider, and
devvault doctor derives the reachability check for that Git host from the
registry (git ssh gitlab.com for an SSH remote, a credential-helper reminder
for an HTTPS one). Without the metadata that check does not fail, it silently
disappears. A directory that is not a Git repository, or a repository without an
origin remote, is registered without Git metadata; that is the normal local
case and not an error.
devvault remove <project-or-alias> deletes the registry entry again. It never
touches the repository on disk.
Selecting and inspecting projects
devvault list # projects grouped by customer
devvault list --json
devvault use kunde-a # set the active project
devvault current # print the active project
devvault remove kunde-a
list marks the active project with * and appends (missing) when the
registered path no longer exists. Alias, credential set and Git provider are
shown when they are known. It stays the cheap listing: it reads your registry
and nothing else.
devvault overview is the view across all of them — one line per registered
project, so "which of my projects need attention" is a single command instead of
a devvault status per project:
devvault overview
devvault overview --json
PROJECT STATE IMAGE TRUST PINS CREDENTIALS
* kunde-a/network_p ok built ok current ok
kunde-a/platform blocked not built drifted 2 behind ok
kunde-b/sandbox blocked - - - -
2 of 3 projects need attention:
- kunde-a/platform: devvault trust review -p kunde-a/platform
- kunde-b/sandbox: devvault registry check
The PINS column compares each project's pinned tool versions against the versions
this devvault ships as defaults. It makes no network request: `devvault tools
outdated -p <project>` is the command that asks upstream for the current
releases.
The active project is marked with *, the same way devvault list marks it.
STATE is the worst of the four columns and has three values: blocked when
devvault would refuse to do the work or the work would fail — a drifted file,
missing credentials, a directory that is gone; warn when something is behind
or undetermined but nothing is in the way; and ok when all four columns are
clean.
devvault status --json calls the same severity missing, because there it
describes one check that looked for one thing. Here it folds four unlike
concerns into one word, and a project whose compose file drifted is not
"missing" anything — so the cross-project table says blocked. The severities
are otherwise identical: ok, warn and unknown carry the same meaning in
both.
| Column | Values | What it says |
|---|---|---|
IMAGE |
built, not built, ? |
Whether the container runtime has the image this project is configured to use. not built is answered by devvault build -p <project> |
TRUST |
ok, stale, drifted, ? |
Whether the repository-owned files devvault executes are still trustworthy — see below |
PINS |
current, N behind, ? |
How many pinned tool versions are older than the ones this devvault ships as defaults — an offline comparison, see below |
CREDENTIALS |
ok, no profile, missing, ? |
Whether the AWS config of this project's credential set exists and holds its profile. It is the same check devvault status runs, so the two can never disagree. The column stays one word and never names a path — devvault status -p <project> is where the detail belongs |
Every suggested command names the project it is for, because in a table of
eleven projects a bare command would act on the active one — which is rarely
the row you were reading. devvault credentials login gets -p <project>;
devvault credentials setup, which has no -p, is addressed with
--customer <customer> --set <credential set>, taken from your registry entry
rather than from the repository's config.
A project usually reports more than one thing, and you get one command for it:
the one that unblocks the most. drifted comes first, because devvault refuses
to run anything in a container for that project until you have looked at the
file — a suggestion to set up credentials would send you to a devvault build
that still refuses, for a reason your row never mentioned. Credentials come
second: they block the work that talks to AWS, but build and shell run
without them. Then stale, which blocks nothing but is worth doing before a
build rather than after it; then the image; and last the pins, which cost
nothing until the next build.
drifted and stale are different problems and have different fixes.
drifted means a file devvault executes — the compose file, the Dockerfile, one
of the helper scripts — is neither what devvault would generate nor something you
approved. devvault refuses to run anything for that project until you looked at
it, so the suggested command is devvault trust review -p <project>. stale
means the file still carries your approval, so nothing is blocked, but it no
longer follows your config — what a devvault tools bump or a hand-edited
.devvault/config.yaml leaves behind. devvault sync -p <project> regenerates
it. See Trusted project files for the whole model.
- and ? are not the same answer. ? means devvault tried and could not
find out: no container runtime on this host to ask about the image, a trust
inspection that failed. - means devvault did not look, because there was
nothing to look at — the registered directory is gone, so the row carries - in
every column and blocked as its state. devvault registry check is the command
for a registration that no longer describes reality.
The PINS column makes no network request. It compares each project's pins
against the versions this devvault build ships as defaults, which is why it is
free enough to run for every project on every invocation. It is therefore not the
upstream check: devvault tools outdated is the command that asks HashiCorp and
GitHub what the current releases are, and it is one of only three commands that
open a connection at all (see Network access). A pin that is
newer than the shipped default is never a finding — running
devvault tools bump puts you ahead of the built-in defaults, and that is
correct rather than a problem.
A registered project whose .devvault/config.yaml devvault cannot read gets no
row at all. Those are listed under the table with the reason, because a missing
row must not be read as a healthy one.
The command only reads, and the exit code is always 0. It starts no
container, builds nothing and writes nothing; the furthest it goes is asking the
container runtime whether an image exists. And "not built yet" is a normal state
rather than a failure, so — like devvault status — the report is the result and
the exit code is not. If you need something a script can gate on, use
devvault registry check, or read
devvault overview --json, whose overall field carries the same verdict per
project.
devvault context answers "what would devvault do here", without starting
anything:
devvault context
devvault context -p kunde-a/network_p --json
It prints customer, project, path, credential set, AWS profile, the path of the
AWS config file, the Terraform version and the selected backend — and
(not registered) where a credential value would stand for a registered
project. It never reads the AWS config file's contents. AWS_ACCOUNT_ID is
echoed as aws_account when the environment variable is set.
devvault status is the readiness view: it runs the individual checks, prints
each one with its state and a next: hint, and ends with a list of suggested
commands.
devvault status
devvault status -p kunde-a/network_p --json
It always reports config, credentials, backend and image. A fifth check,
terraform, appears only when there is something to say about it: your project
pins a CLI version in .devvault/config.yaml, and the repository states which
versions it accepts — in a required_version block, or in a .terraform-version
/ .opentofu-version file. When the pin does not satisfy that constraint, the
container builds fine and then fails minutes later in terraform init, with
Terraform's own error. devvault says it first:
[warn] terraform: terraform 1.0.11 does not satisfy ">= 1.5.0" from "versions.tf", so terraform init will fail in the container
next: run devvault tools bump -p <project> --only terraform for the current release, or set terraform.version in .devvault/config.yaml to a version the constraint allows; then devvault sync and devvault build, because the image keeps the old version until it is rebuilt
It is a warning, never an error — the exit code does not change, and neither
does anything else devvault does. Terraform explains the mismatch better than
devvault can once it actually runs; the point of checking here is that it runs
only after a build of several minutes. For the same reason devvault build
prints the identical warning on stderr before it starts building, and builds
anyway: you may be building exactly that image on purpose.
The comparison follows terraform.distribution, so an OpenTofu project is
measured against terraform.opentofu_version and never against a Terraform
release number.
There is deliberately no "ok" line for this check. devvault understands the
common constraint syntax — =, !=, >, >=, <, <=, the pessimistic ~>,
and comma-separated conditions that all have to hold — with two- or
three-component versions, where ~> 1.5 means "any 1.x from 1.5 on" and
~> 1.5.0 means "any 1.5 patch". Anything beyond that — an unknown operator, a
prerelease like 1.5.0-rc1, a leading v, a constraint devvault cannot parse —
produces no line at all, rather than a green one. A repository that states no
version produces no line either. Silence here means "not checked", and never
"checked and fine".
devvault version prints the version and build information of the binary
itself; devvault --version prints the short form.
Health checks and repairs
devvault doctor probes the host: macOS version, architecture, git, docker,
Apple container, the folded "at least one container backend" check, the
configured editor, the SSH agent, the credentials mount directory, your project
registry and the optional host tools you enabled in tools.*.
It adds one check per Git host that appears in your registry: an SSH remote is
probed with ssh -T and reported as git ssh <host>, an HTTPS remote as a
reminder to have a credential helper or token configured. Those hosts come from
the Git metadata devvault add, devvault clone, devvault setup and
devvault quickstart record — a project whose repository has no origin remote
contributes none.
devvault doctor
devvault doctor --json
devvault doctor --fix
Architecture, git and the container backend are the required checks, and they
alone decide the exit code — a host that fails one of them cannot run devvault
at all. Everything else is a warning: a single missing container runtime is
normal when the other one is installed, and editor, SSH agent and credentials
mount are per-project or convenience concerns.
The project registry line is the same rule applied to devvault's own
bookkeeping. It counts what devvault registry check
found and points at that command for the detail; it is never a required check,
because an inconsistent registry is not a property of this host and must not
change whether a script considers the machine usable.
[warn] project registry: 4 problem(s): 3 error(s), 1 warning(s)
next: run devvault registry check for the findings and the command that fixes each one
The "SSH agent" check also looks at the active backend. With Apple container
selected and ssh.forward_agent enabled it warns that the agent cannot be
forwarded even though your host has one, because that combination gives you a
container without an agent; see
Choosing a container backend.
--fix performs only the repairs devvault can make safely and locally — today
that is creating a missing AWS credentials mount directory. It never installs
software, starts a daemon, touches the network or writes into a repository; for
those it keeps printing the hint. After repairing it probes again, so the printed
report is the state after the run and not a prediction.
devvault fix is the project-level counterpart:
devvault fix
devvault fix --dry-run
devvault fix --regenerate
devvault fix --json
devvault fix --login
It creates the credentials mount directory when it is missing, reports which of
the expected generated files are absent, and with --regenerate writes them
again. --regenerate requires a registered project, because regenerating writes
the credentials mount into the project's files. --dry-run reports without
changing anything, and --login runs the AWS login afterwards.
devvault backend doctor checks the Terraform backend configuration by running
terraform init -backend=false inside the container; see
The Terraform lifecycle.
Checking the registry itself
devvault doctor checks the host, devvault trust status checks the files a
repository ships and devvault status checks whether one project can start.
None of them looks at the registry under ~/.devvault — the file that decides,
for every project, which directory devvault works in and which credentials it
mounts. devvault registry check is that missing check.
devvault registry check
devvault registry check --json
It reports:
| Finding | Severity | Why it matters |
|---|---|---|
missing_path |
error | The registered directory does not exist, cannot be read, or is not a directory. devvault list marks it (missing); here it comes with the way out |
duplicate_path |
error | Several entries point at the same directory. Which identity — and with it which credential set gets mounted — then depends on the entry a command happens to name |
unreadable_config |
error | The directory is registered but has no readable .devvault/config.yaml, so every command that loads the project fails |
dangling_active_project |
error | devvault use selected a project that is no longer registered, so every command without -p fails |
alias_collision |
error | Two projects share one alias. devvault refuses an ambiguous key rather than guessing, so the alias stops working for both |
missing_git_repository |
warning | The entry records a Git remote, but the directory is no longer a checkout. devvault doctor keeps probing a host this project does not use any more |
missing_credential_mount |
warning | The credentials directory of a set that projects use is not on disk, so those projects get a container without an AWS config |
orphaned_credential_set |
warning | A credential set no registered project uses, left behind by a project that was renamed or removed |
A real run against a registry with several of those:
$ devvault registry check
[error] platform/platform: the registered path "/private/tmp/devvault-demo/code/platform" does not exist; the directory was moved or deleted. Register it again at its new location with devvault add <new-path> --id platform/platform, or drop the entry
fix: devvault remove platform/platform
[error] /private/tmp/devvault-demo/code/network_p: 2 registry entries point at the same directory "/private/tmp/devvault-demo/code/network_p": gitlab/devvault-test, network_p/network_p. Which identity — and with it which credential set devvault mounts — then depends on the entry a command happens to name
fix: devvault remove gitlab/devvault-test
[error] network_p: 2 projects share the alias "network_p": gitlab/devvault-test, network_p/network_p. devvault refuses an ambiguous key, so every command naming that alias fails instead of picking one of them
fix: devvault add /private/tmp/devvault-demo/code/network_p --id network_p/network_p --alias <unique-alias>
[warning] platform/platform: the credentials directory "~/.aws-kunden/platform/platform" of this set does not exist, so the 1 project(s) using it get a container without an AWS config: platform/platform
fix: devvault fix -p platform/platform
4 problems: 3 errors, 1 warning
devvault changed nothing: each command above removes or rewrites a registration,
which stays your decision.
Once the registry is in order the command says so and exits 0:
$ devvault registry check
registry is consistent
There is no --fix, on purpose. Every command in the report deletes or
rewrites a registration, and a registration is where you state which customer
and credential set a directory belongs to — the assertion devvault derives the
credentials mount from, see Registered projects. devvault
names the command and leaves the decision to you. registry check writes
nothing at all, not even the registry it just read.
Note that removing a project leaves its credential set in the registry: the
files under ~/.aws-kunden are yours, and devvault does not delete them behind
your back. The next run reports that set as orphaned_credential_set, which is
a note, not damage.
--json prints the same findings as one document, with a stable kind per
finding, so a script can act on the categories instead of on wording:
$ devvault registry check --json
{
"ok": false,
"errors": 3,
"warnings": 1,
"findings": [
{
"kind": "missing_path",
"severity": "error",
"subject": "platform/platform",
"projects": [
"platform/platform"
],
"message": "the registered path \"/private/tmp/devvault-demo/code/platform\" does not exist; the directory was moved or deleted. Register it again at its new location with devvault add <new-path> --id platform/platform, or drop the entry",
"fix": "devvault remove platform/platform"
},
...
]
}
The exit code is 1 as soon as there is any finding, warnings included; see
Exit codes.
Moving to another machine
Your registry is the mapping between directories, customers and credential sets, and on a new Mac it starts out empty: every project has to be registered again by hand. Two commands carry that mapping across.
devvault registry export -o registry.yaml # on the old machine
devvault registry import registry.yaml # on the new one
What does not travel, and why
The obvious shortcut — copying ~/.devvault/projects.yaml — is the wrong move,
and this is the part to read before treating an export as a backup. That file
holds five things that are true only on the machine that wrote them, and the
export leaves every one of them behind:
| Left out | Why |
|---|---|
git_url and git.url |
An HTTPS clone URL can carry a token in its userinfo part, so the clone URL never leaves the machine — the same reason devvault list --json does not print it. provider, host, owner, repo and clone_method identify the same remote, and registry import rebuilds the URL from them where it needs one |
trust |
An approval means you looked at that file content, on that machine, and accepted it. Carried over it would pre-approve repository content nobody here has seen. On the new machine everything matching the rendered template is trusted automatically anyway; the rest is meant to be looked at again — see Trusted project files |
images |
Records of images devvault built here. Elsewhere they name images that do not exist, and devvault images prune reads them as "safe to delete" |
path and last_opened |
An absolute path of the old machine says nothing about the new one. Only the last element travels, as directory:, and only as a hint for finding the checkout again; last_opened is dropped without replacement |
mount, unless it is portable |
A credentials mount travels as long as it holds elsewhere: a ~/ path as it is, a path below your home directory rewritten to a ~/ one. A mount that names a directory outside your home — /Users/olduser/.aws-kunden/kunde-a/alpha after you moved once already — is left out, because it would arrive pointing at nothing. Nothing is lost: a credential set without a mount is one devvault derives from customer and credential set, ~/.aws-kunden/<customer>/<set>, which is the directory that exists on the new machine. registry export says how many mounts it left out |
And no credential material, in either direction. Profile, credential set and
mount names are the mapping and travel with it; nothing below ~/.aws-kunden
is ever read or written. Both commands say so in their output, because an export
that looked like a backup would be a dangerous thing to believe.
The document
devvault registry export writes YAML to stdout so it can be piped; the note
about what is not in it goes to stderr, so a redirected file holds exactly the
document. -o <file> writes it directly, with mode 0600. --json writes the
same content as JSON — registry import reads either.
$ devvault registry export
kind: devvault-registry-export
format_version: 1
active: kunde-a/alpha
customers:
- id: kunde-a
name: Kunde A GmbH
default_workspace: dev
credential_sets:
- id: alpha
profile: kunde-a-dev
mount: ~/.aws-kunden/kunde-a/alpha
auth_method: sso
projects:
- id: kunde-a/alpha
customer: kunde-a
name: alpha
alias: alpha
credential_set: alpha
backend: auto
directory: alpha
git:
provider: gitlab
host: gitlab.example.com
owner: kunde-a
repo: alpha
clone_method: https
A credentials mount below your home directory is written as a ~/ path, so it
lands below the new home rather than repeating the old machine's user name. A
mount somewhere else is not exported at all, and the command tells you so:
$ devvault registry export -o registry.yaml
wrote 3 projects and 2 credential sets to registry.yaml
left the mount of 1 credential set out: it is an absolute path outside
your home directory and holds only on this machine, so devvault derives the
mount from customer and credential set on the other one.
This is a mapping, not a backup: it carries no credentials, no trust approvals
and no image records, and devvault neither reads nor writes ~/.aws-kunden.
Without -o that note goes to stderr along with the standing one, so a
redirected file still holds exactly the document.
format_version lets a later devvault recognise an older document, and kind
lets registry import refuse a file that is not an export — most importantly
~/.devvault/projects.yaml itself, which parses fine and carries exactly the
five records above.
Restoring them
devvault registry import <file> looks for each project's checkout below a base
directory — by default ~/DevVault, the same one devvault clone puts
repositories in, and --base-dir names another one. Per project it tries the
repository name from the remote, the directory name the project had, and the
project name, each directly below the base directory and once more below a
directory named after the customer. A candidate counts only when it is a
directory with a readable .devvault/config.yaml.
$ devvault registry import registry.yaml
base directory: /Users/you/DevVault
[registered] kunde-a/alpha: registered "/Users/you/DevVault/alpha"
[not found] kunde-b/beta: no checkout below "/Users/you/DevVault"; devvault registered nothing, because an entry pointing at a directory that is not there is what devvault registry check reports
next: devvault clone git@github.com:kunde-b/beta.git --id kunde-b/beta
credential sets: kunde-a/alpha
active project: kunde-a/alpha
devvault created no directory below ~/.aws-kunden: devvault status names the
credentials a project is missing, devvault credentials setup writes them.
2 projects: 1 registered, 0 skipped, 1 not found
This is a mapping, not a backup: it carries no credentials, no trust approvals
and no image records, and devvault neither reads nor writes ~/.aws-kunden.
A project without a checkout is not registered. An entry pointing at a
directory that does not exist is precisely the missing_path finding of
devvault registry check, and devvault does not
create the state it reports as broken. The line names the devvault clone
command that fetches the repository instead, rebuilt from the Git fields; when a
directory of the right name is there but has no devvault config, the report says
that and names devvault init plus devvault add instead. Run the import again
afterwards — it is repeatable.
An existing registration is never overwritten silently, because a
registration is where you state which customer and credential set a directory
belongs to, and that decides which host directory gets mounted into a container
running that repository's code (see Registered projects).
Such a project is reported and skipped; --yes replaces the entry of the same
id. Replacing keeps the image records — they are the only handle
devvault images prune has on images built here — and keeps a trust approval
only while the directory stays the same.
A directory that is already registered under a different id is left alone even
with --yes. A second entry for one directory is the duplicate_path finding
of registry check: which identity, and with it which credential set devvault
mounts, would then depend on the id a command happens to name. The report names
devvault remove <other-id> and leaves that decision to you.
Credential sets are restored as mapping only, and only for the projects that
were actually registered — importing the set of a project that is not on this
machine would leave you with the orphaned_credential_set finding. A set that
already exists here is never overwritten: it describes files on this machine.
No directory below ~/.aws-kunden is created and no credentials are written;
devvault status names what a project is missing and
devvault credentials setup writes it.
The active project is only taken over when this registry has none, so an import never moves the project you are working on out from under you.
--dry-run prints the same report and writes nothing, which makes it a usable
pre-check. The exit code is 1 as soon as one project was not restored — skipped
ones included — in a dry run as well; see Exit codes.
devvault registry import registry.yaml --dry-run # what would happen
devvault registry import registry.yaml --base-dir ~/work/repos # search elsewhere
devvault registry import registry.yaml --yes # replace what is there
A document whose values devvault cannot use is refused as a whole rather than imported in halves: a customer, project name, credential set, profile or mount has to pass the same allowlist a repository's config passes, because those values decide which directory gets mounted and reach a container CLI as arguments. The error names the entry and the field.
Credentials
Credential material lives on the host, one directory per customer and credential set, and is mounted into the container:
~/.aws-kunden/<customer>/<credential-set>/config
devvault credentials setup writes such a config file. devvault setup and
devvault quickstart call the same code, so this command is mainly for adding a
second credential set to an existing customer, or for repairing one.
Guided setup
Run it without the flags it needs and devvault asks, in a terminal:
devvault credentials setup
It asks for the customer and the credential set first — prefilled from the active project, because adding a second set to the customer you are currently working with is the common case — then lets you pick the auth method from a list where each entry explains itself:
Auth method
> sso — browser login via IAM Identity Center, tokens expire on their own
assume_role — take a role starting from a profile you already have
credential_process — an external command prints the credentials
existing — you maintain the AWS config yourself, devvault only mounts it
Only the fields of the chosen method are asked afterwards. Pick sso and
you answer a start URL, an SSO region, an account id and a role name; --role-arn
never appears. Pick assume_role and you answer the role ARN and the source
profile, and no SSO question shows up. existing adds no questions at all —
devvault does not write that file, it only mounts the one you maintain. Profile
name and region come prefilled (<customer>-<set> and eu-central-1), so the
second form is mostly confirmation.
Long-lived access keys are not on the list. credentials setup does not write
aws_access_key_id / aws_secret_access_key into a file it manages, so offering
that choice would only lead into a refusal.
The guided run ends with one question:
Check these credentials against AWS now?
Answering yes runs the same verification as devvault credentials status:
aws sts get-caller-identity with the profile that was just written. This is the
only step of the command that talks to AWS, it happens only because you asked for
it, and it never happens on the flag-driven path. Say no and devvault names
devvault credentials status -p <project> as the next step instead.
Flags, for scripts
Nothing about the flag-driven path changed: with every required flag set the command runs unattended and asks nothing.
devvault credentials setup \
--customer kunde-a \
--set network_c \
--profile network-c-dev \
--auth assume_role \
--role-arn arn:aws:iam::123456789012:role/DevAccess \
--source-profile kunde-a-sso \
--region eu-central-1
| Flag | Meaning |
|---|---|
--customer |
Customer id |
--set |
Credential set name |
--profile |
AWS profile |
--mount |
Credentials mount; defaults to ~/.aws-kunden/<customer>/<set>, and replaces --customer / --set when you name it yourself |
--auth |
Auth method: sso, assume_role, credential_process or existing |
--sso-start-url, --sso-region, --sso-account-id, --sso-role-name |
AWS SSO parameters |
--role-arn, --source-profile |
assume_role parameters |
--credential-process |
credential_process command |
--region |
AWS region, default eu-central-1 |
--force |
Overwrite an existing config |
Without a terminal — in a script, in CI, behind a pipe — an incomplete call is refused instead of opening a form nobody can answer, and nothing is written:
$ devvault credentials setup --customer kunde-a --set network_c --profile network-c-dev </dev/null
devvault credentials setup is missing --auth, and there is no terminal to ask in; pass the flags, or run the command in a terminal for the guided setup
--auth decides what else is needed:
--auth sso adds --sso-start-url, --sso-region, --sso-account-id, --sso-role-name
--auth assume_role adds --role-arn, --source-profile
--auth credential_process adds --credential-process
--auth existing adds no further flags
When --customer and --set are both given, the credential set is also recorded
in your registry, which is what makes it available to projects.
The command prints the path it wrote and never the content of that file — an AWS config names your SSO portal, your account and your role.
Listing, checking and logging in
devvault credentials list # every customer
devvault credentials list kunde-a # one customer
devvault credentials list --json
devvault credentials status -p kunde-a/network_c # verify the profile works
devvault credentials login -p kunde-a/network_p # aws sso login, or the equivalent
credentials status fails with an actionable message when the AWS config file
does not exist or does not contain the configured profile, then calls the AWS CLI
to verify the identity. credentials login runs the login for the project's auth
method. Both refuse for an unregistered directory — they would otherwise have to
point the AWS CLI at a config file the repository chose. Neither command ever
prints the contents of a credentials file.
Noticing a missing config before Terraform does
You do not have to remember to check. The credentials line of
devvault status reports it on every run,
without touching the network:
[missing] credentials: AWS config missing: /Users/you/.aws-kunden/kunde-a/network_c/config
next: run devvault credentials setup or devvault setup
[warn] credentials: profile network-c-dev not found in /Users/you/.aws-kunden/kunde-a/network_c/config
next: run devvault credentials setup --force or update the AWS config
A file that exists and contains the configured profile reports [ok] with the
profile and the path. That is the cheap way to find out that a credential set is
missing, or names a different profile than the project expects — before
terraform plan runs into it inside the container.
Working in the container
Everything below runs inside the project's development container. The first command builds it:
devvault build # build the image
devvault shell # interactive shell inside the container
Before any of these commands starts a container, devvault checks that the project is registered and that the repository-owned files it is about to execute are trusted; see Registered projects and Trusted project files for what a refusal means.
devvault build additionally compares the pinned Terraform version with the
required_version the repository declares, and prints a warning on stderr
before the build when the two disagree — an image that takes minutes to build
and then fails in terraform init is the expensive way to find that out. The
warning never stops the build; it is the same check and the same wording
devvault status reports, including when it stays
silent.
devvault tf runs an arbitrary Terraform command:
devvault tf version
devvault tf fmt -recursive
devvault tf output -json
devvault tf -p kunde-a/network_p providers
The three fixed chains are:
devvault validate # terraform fmt -check -recursive, init -backend=false, validate, tflint --recursive
devvault lint # tflint --recursive, plus trivy fs . and checkov -d . when enabled
devvault docs # terraform-docs markdown table --output-file README.md --output-mode inject .
validate stops at the first failing step. lint adds a scanner only when its
switch in tools.* is true, so a minimal project runs tflint alone. docs
injects into the README.md of the project directory, between the terraform-docs
markers.
Choosing a container backend
backend in .devvault/config.yaml decides which runtime starts the container:
auto (the default) prefers Apple container and falls back to Docker Desktop,
apple-container and docker pin one of them. devvault doctor prints which
one is active. The two are equivalent for everything devvault does inside the
container, with one difference that is worth deciding on before you pick:
Only the docker backend forwards your SSH agent. Apple container cannot
proxy a host Unix socket into the guest — mounting one does not merely fail to
forward the agent, it makes the container fail to boot with proxyVsock: failed to setup vsock proxy — so devvault never mounts the agent socket there,
whatever ssh.forward_agent says. In practice that means Git operations run
inside the container against a private repository (terraform init pulling a
module from a private Git source, a git command in devvault shell) have no
credentials on the Apple container backend and will fail. Work that only talks
to AWS and the Terraform registry is unaffected.
When ssh.forward_agent is enabled and your host actually runs an agent,
devvault says so once per run before it starts the container:
warning: the Apple container backend cannot forward an SSH agent socket, so it is not mounted.
warning: git operations against private repositories will fail inside the container.
warning: use the docker backend for those, or set ssh.forward_agent: false to silence this.
The two ways out are exactly those. Switch the project to Docker Desktop:
# .devvault/config.yaml
backend: docker
or, if you never clone private repositories from inside the container, turn the request off and keep the warning quiet:
# .devvault/config.yaml
ssh:
forward_agent: false
Run devvault sync and devvault build after changing either key. Cloning and
devvault clone are unaffected — those run on the host, with your normal agent.
On the Apple backend the configured container name is a prefix. devvault
appends a short random suffix to every container it starts, so
apple_container.container_name: devvault-network produces names such as
devvault-network-4k7pq2ab. It has to: a single devvault command starts several
containers in a row — devvault validate runs four — and Apple container
releases the name of a finished --rm container asynchronously. Under one fixed
name the next step fails with container with id … already exists, for a
container that container list --all no longer shows and container delete no
longer finds. Two devvault calls in the same project, one per terminal, would
collide on that name outright. The prefix is what you recognise in container list while a run is in progress; if it already uses all 64 characters an
identifier allows, devvault shortens it to make room for the suffix. The docker
backend is unaffected and uses docker.container_name as written.
The shared provider cache
Both backends mount a persistent Terraform plugin cache at
/home/vscode/.terraform.d/plugin-cache and point TF_PLUGIN_CACHE_DIR at it,
so the providers terraform init downloads survive the container and are shared
by every project of the same customer instead of being fetched again on each run.
This is not only a speed-up, it is what makes the container usable at all: each
devvault step starts its own throwaway container, and terraform init leaves in
the repository's .terraform/providers only links into that cache. Without a
cache that outlives the container, the plugins vanish with the container that
downloaded them and the next step — validate, plan, anything — stops with
missing or corrupted provider plugins.
Where the cache lives depends on the backend, and both are named by a config key
that defaults to devvault-<customer>-tf-cache:
| Backend | Key | Cache |
|---|---|---|
| docker | docker.plugin_cache_volume |
a docker volume, declared and mounted by the generated compose file |
| apple-container | apple_container.plugin_cache_name |
a directory below your devvault home: ~/.devvault/plugin-cache/<name>, created with mode 0700 and bind-mounted |
The Apple backend uses a host directory rather than a container volume on
purpose. Apple container creates a named volume as an empty, root-owned
filesystem and — unlike docker — does not seed it from the image, so the
container's non-root vscode user cannot write into it; a volume would need a
--user root container just to fix its ownership. A bind-mounted directory is
presented to the guest as owned by the container's user, which is why the
workspace mount works, and it leaves the cache visible on your host.
devvault creates the cache if it does not exist yet, so there is nothing to set up.
devvault cache list
A cache only ever grows: every provider version an init pulls is added, nothing
is ever removed. cache list shows what that costs and who still needs it.
devvault cache list
devvault cache list --json
cache directory: /Users/dev/.devvault/plugin-cache
NAME SIZE USED BY
devvault-altkunde-tf-cache 17.0 MiB (orphaned)
devvault-kunde-a-tf-cache 733.2 MiB kunde-a/network_p, kunde-a/platform
total: 750.2 MiB in 2 caches
docker volumes named by registered projects for the same cache:
docker volume rm devvault-kunde-a-tf-cache # used by kunde-a/network_p
devvault does not manage these: the volume belongs to docker, and `docker volume rm`
is the command that removes one.
USED BY lists the registered projects whose config names that cache. A cache no
registered project names — the customer you stopped working for, a project you
removed from the registry — is (orphaned) and is what cache prune removes by
default. The reference is counted whatever backend a project is configured for:
both config keys carry the same name, backend: auto decides per host, and the
harmless mistake is to keep a cache nobody needs rather than delete one a project
would have used.
Reading that list means reading the config of every registered project. A project whose directory moved away, or whose config no longer parses, is reported as skipped instead of aborting the command — with the note that a cache it uses can then look orphaned:
skipped 1 registered project(s) whose config devvault could not read:
kunde-a/altprojekt: read config: no file found in any of [...]
a cache one of them uses can therefore look orphaned.
devvault cache prune
devvault cache prune # only the orphaned caches
devvault cache prune --yes # ... without the confirmation prompt
devvault cache prune --all --yes # every cache, including the ones in use
devvault cache prune -p network_p --yes # exactly the cache that project uses
devvault cache prune --json --yes
Without a flag devvault deletes only the orphaned caches. --all takes the ones
in use as well, and -p <project> exactly the cache that project's config names;
the two cannot be combined, because together they would have to mean one thing or
the other. Every run lists what it is about to delete and how much that frees
before it deletes anything:
devvault will delete 1 cache from /Users/dev/.devvault/plugin-cache:
devvault-altkunde-tf-cache 17.0 MiB (orphaned)
this frees 17.0 MiB
removed /Users/dev/.devvault/plugin-cache/devvault-altkunde-tf-cache (17.0 MiB)
freed 17.0 MiB
the next terraform init downloads those providers again, so the first run after
a prune is slower than usual
Deleting a cache cannot be undone, so prune asks before it does it, and without
a terminal to ask in it refuses:
devvault cache prune deletes 1 cache (17.0 MiB) and cannot be undone; rerun with --yes to confirm
Nothing is lost either way — the next terraform init downloads the providers
again — but that run is slow, and a slow run you did not expect looks like a
fault. That is why the command says so afterwards.
A devvault home that has never run a container has no cache directory at all.
That is not an error: cache prune reports that there is nothing to clean up and
exits 0, so it is safe in a script.
Docker volumes are listed, never deleted. On the docker backend the cache is
a docker volume, and that volume is docker's state, not devvault's — docker has
docker volume rm for the job, and devvault does not delete another tool's data.
Staying quiet about them would be worse, though: a prune that silently ignored
half your caches would be misleading. So both commands name every volume a
registered project asks for, together with the exact command that removes one.
devvault does not call docker to do that — the names come from the configs of
your registered projects, so the list works with docker stopped or absent.
The images devvault builds
Every devvault build produces a container image of a gigabyte or more, and
nothing ever removes one. Rename a project, change docker.image_name, and the
next build produces a second image while the first stays on disk under the old
name — invisible unless you go looking for it with container image list.
devvault images is where you look.
devvault deletes only images it recorded having built itself. This is the one
rule the whole command is built around, and it is what makes it different from
cache prune. The provider cache lives in devvault's own directory below
~/.devvault, so everything in there is devvault's by construction. Images do
not: they live in the shared storage of the container runtime, next to a database
image, a base image somebody pulled, another tool's build output.
Recognising an image by its name is not good enough, however much the
devvault- prefix invites it. docker.image_name and
apple_container.image_name are ordinary config keys that any repository may set
to anything, so a name that looks like devvault's may belong to somebody else —
and a wrong match deletes work that cannot be recovered. So after a successful
build devvault writes the image name into that project's entry in
~/.devvault/projects.yaml:
projects:
kunde-a/network_p:
path: /Users/dev/code/network_p
images:
- devvault-netzwerk-prod
That record is the only evidence devvault accepts. It is also what catches the rename: the old name stays in the list while the config already names a new one, which is exactly what "orphaned" means below.
devvault images list
devvault images list
devvault images list --json
IMAGE PROJECT RUNTIME SIZE STATE
devvault-netzwerk-prod kunde-a/network_p apple-container 185.5 MiB in use
devvault-platform kunde-a/platform apple-container - not built yet
devvault-altkunde-net kunde-a/platform apple-container 1.6 GiB orphaned
1 image orphaned, 1.6 GiB reclaimable with `devvault images prune`
devvault lists the images of its registered projects and the images it recorded
building. Everything else in the container runtime belongs to somebody else and
never appears here — use `container image list` or `docker image ls` for those.
One line per registered project for the image its config names, plus one for every image devvault recorded that no project names any more. Rows are grouped by project; the configured image comes first, the leftovers after it.
RUNTIME is the backend that project resolves to on this host, and it decides
which of the two config keys is shown: apple_container.image_name or
docker.image_name. Sizes come from container image inspect (summed over the
variants of a multi-architecture image) and docker image inspect.
SIZE is never invented: - means the runtime answered that the image is not
there, and unknown that devvault could not ask or got no number back. STATE
is the answer to "may I delete this":
STATE |
Meaning |
|---|---|
in use |
A registered project's config names this image, and devvault built it |
orphaned |
devvault built it, no registered project names it any more — this is what prune removes by default |
not built yet |
The project's configured image, which devvault has not built |
not recorded, devvault will not delete it |
The image exists, but devvault has no record of building it. See the limit below |
..., already gone |
devvault holds a record for an image the runtime no longer has; prune clears the record |
no container runtime available |
No backend is reachable on this host, so nothing could be asked. prune refuses these rather than assume they are gone |
An image name is counted as in use whatever backend the naming project is
configured for: both config keys normally carry the same name, backend: auto
decides per host, and the harmless mistake is to keep an image nobody needs
rather than delete one a project would have used. Reading that means reading the
config of every registered project, and a project whose directory moved away or
whose config no longer parses is reported as skipped instead of aborting the
command — with the note that an image it uses can then look orphaned.
devvault images prune
devvault images prune # only the orphaned images
devvault images prune --yes # ... without the confirmation prompt
devvault images prune --all --yes # every recorded image, including the ones in use
devvault images prune -p network_p --yes # every image recorded for that project
devvault images prune --json --yes
Without a flag devvault deletes only the orphaned images. --all takes the ones
in use as well, and -p <project> every image devvault recorded for that
project; the two cannot be combined, because together they would have to mean one
thing or the other. Every run lists what it is about to delete and how much that
frees before it deletes anything:
devvault will delete 1 image:
devvault-altkunde-net kunde-a/platform 1.6 GiB orphaned
this frees 1.6 GiB
removed devvault-altkunde-net (1.6 GiB) from apple-container
freed 1.6 GiB
the next devvault build builds these images again, which takes minutes rather
than seconds
Deleting an image cannot be undone, so prune asks before it does it, and
without a terminal to ask in it refuses:
devvault images prune deletes 1 image (1.6 GiB) and cannot be undone; rerun with --yes to confirm
Nothing is lost either way — the next devvault build builds the image again —
but that build takes minutes rather than seconds, which is why the command says
so afterwards.
After the runtime confirms the deletion, devvault drops the record from
~/.devvault/projects.yaml, so the entry never outlives the image it described.
The order matters: the record is the evidence that the image is devvault's, so it
goes last. If the runtime refuses — a container is still using the image —
devvault reports that image, keeps its record and carries on with the rest;
the command then exits 1 while everything it could delete is gone:
removed devvault-platform (1.6 GiB) from docker
freed 1.6 GiB
remove the docker image "devvault-netzwerk-prod": exit status 1: Error response from daemon: conflict: unable to remove repository reference "devvault-netzwerk-prod" (must force) - container 9a1 is using its referenced image
devvault never passes --force or -f to the runtime. Forcing would remove an
image a container is still running on, and that container is not devvault's to
break — stop it yourself and rerun.
What this cannot find
Images built before devvault recorded them are invisible to this command.
Anything devvault build produced under 0.4.1 or older has no entry, so
images list does not show it and
images prune does not remove it. There is deliberately no name-pattern
fallback to paper over the gap — guessing "this looks like ours" is exactly the
mistake that costs somebody an unrelated image. Find and remove those by hand:
container image list # Apple container
container image delete devvault-old-name
docker image ls # docker
docker image rm devvault-old-name
A rebuild also fixes it for the current name: the next devvault build records
the image it produces, so from then on the project's own image is tracked.
The same blind spot applies to devvault remove: dropping a project from the
registry drops its image records with it. Run devvault images prune -p <project> --yes before unregistering a project whose image you no longer want, or clean it
up by hand afterwards.
The Terraform lifecycle
plan
devvault plan
devvault plan -var-file=prod.tfvars -target=module.network
devvault plan --save
devvault plan --save --plan-file .devvault/plans/prod.tfplan
--save runs terraform plan -out=<file> and defaults the file to
.devvault/plans/latest.tfplan; --plan-file overrides the location. Both are
devvault's own flags, and like every other flag devvault interprets they stop at
a --: devvault plan -- --save reaches Terraform as terraform plan --save
and writes no plan file (see
Flags that apply almost everywhere).
A saved plan regularly contains secrets in plaintext — resolved variables and
provider responses end up in the file. devvault therefore creates the plan
directory with mode 0700, drops a .gitignore into it that ignores everything
except itself, and restricts the written plan file to 0600. An existing
directory keeps its mode and an existing .gitignore is left untouched, so a
--plan-file pointing somewhere else is not silently re-permissioned; the plan
file itself is restricted either way. If devvault cannot tighten the permissions,
or no plan file appears at all, it says so on stderr instead of failing silently.
Never commit a plan file and never hand one out.
apply, apply-plan, destroy, import
devvault apply --yes
devvault apply --yes -target=module.network
devvault apply-plan --yes
devvault apply-plan --yes --plan-file .devvault/plans/prod.tfplan
devvault apply-plan --yes --max-plan-age 2h
devvault apply-plan --yes --allow-stale-plan
devvault destroy --yes
devvault import --yes aws_s3_bucket.logs my-log-bucket
All four require --yes; see
Flags that apply almost everywhere. Before
running, they print the project id, customer, workspace, AWS profile and
credential set, so the account you are about to change is visible in the output
and in CI logs.
apply-plan applies a plan saved with plan --save and defaults to the same
.devvault/plans/latest.tfplan. Delete the plan file once it has been applied.
If there is no plan at that path, devvault says so and names
devvault plan --save instead of starting a container that fails on a missing
file.
Why devvault checks how old a saved plan is
A saved plan is self-contained. terraform apply plan.out never re-reads the
.tf files: everything it will do was decided when the plan was made. Terraform
catches half of the danger by itself — if the state moved on, it refuses the
plan. What it never notices is that the configuration changed. Edit a module,
forget to re-plan, apply yesterday's file, and Terraform applies yesterday's
intent against today's code without a word of warning. That is the case devvault
can see and Terraform cannot, so apply-plan looks at the plan file before it
hands it to the container and refuses a stale one:
- the plan is older than
--max-plan-age, which defaults to24h; or - a Terraform file is newer than the plan — any
*.tf,*.tf.json,*.tfvars,*.tfvars.jsonor.terraform.lock.hclin the project whose modification time lies after the plan's..git/,.terraform/,.devvault/and the directory the plan itself lives in are not looked at: they hold Git and Terraform machine state and devvault's own generated files, none of which change what a plan would do.
The refusal names both — how old the plan is and which files overtook it, at
most five of them plus an and N more — and it names the way out:
$ devvault apply-plan --yes
the saved plan .devvault/plans/latest.tfplan is 48h0m1s old, past the 24h0m0s
limit of --max-plan-age, and 1 Terraform file(s) changed after it was written:
"main.tf".
terraform apply never re-reads the Terraform files: it applies the plan as it
was made, so a configuration that changed in the meantime is applied as it
looked back then. Terraform refuses a plan whose state moved on, but it cannot
see this.
Make a current plan:
devvault plan --save
Or apply this one anyway, if you are sure the difference does not matter:
devvault apply-plan --yes --allow-stale-plan
--max-plan-age sets the age limit for a single run; --max-plan-age 0 turns the
age check off and leaves the check for newer Terraform files in force.
--allow-stale-plan applies the plan anyway. devvault then prints the same
sentence as a warning on stderr instead of refusing, before the customer and
account block, so the record of what you overrode stays in the terminal and in
CI logs.
--max-plan-age <duration> takes a Go duration (90m, 2h, 36h) and
sets the age limit for that one run. --max-plan-age 0 switches the age check
off — and only that half: a Terraform file that is newer than the plan still
stops the run, because that check is about the plan not matching the code rather
than about the plan being old. Use it for a plan that is deliberately kept for a
while, for instance one made in CI and approved by a human hours later.
--yes does not double as the opt-in. The two flags answer different questions:
--yes says "I know this changes infrastructure", --allow-stale-plan says "I
know this plan may no longer match the code". Passing --yes alone therefore
still gets the refusal above.
The age limit is a flag and deliberately not a key of
.devvault/config.yaml. That file belongs to the repository, and devvault treats
every cloned repository as untrusted input (see
Registered projects). A repository that
could write plan.max_age: 8760h would switch this protection off for the very
person it protects, quietly, in the same file that describes the plan it wants
applied. Flags come from you, config comes from the repo — so the limit lives on
the command line.
workspace and state
devvault workspace list
devvault workspace show
devvault workspace select prod
devvault state list
devvault state pull > state.json
devvault state mv --yes aws_s3_bucket.old aws_s3_bucket.new
devvault state rm --yes aws_s3_bucket.legacy
state mv and state rm change state and therefore require --yes; state list and state pull do not. All of them forward their remaining arguments to
Terraform unchanged.
backend, modules and graph
devvault backend doctor # terraform init -backend=false
devvault modules doctor # terraform fmt -check -recursive, then tflint --recursive
devvault modules changelog # prints the current changelog guidance
devvault graph # terraform graph | dot -Tsvg > terraform-graph.svg
graph needs Graphviz in the image. When tools.graphviz is false, devvault
says which switch is off and that the image has to be rebuilt, instead of letting
the container fail with a bare "not found". modules changelog currently prints
guidance only; module changelog automation is not implemented.
Security and quality
devvault scan # the enabled scanners, in one run
devvault secrets scan # gitleaks detect --source .
devvault policy check # conftest test .
devvault precommit install
devvault precommit run # pre-commit run --all-files
devvault scan runs trivy config . when tools.trivy is true, checkov -d .
when tools.checkov is true and gitleaks detect --source . when
tools.gitleaks is true. A project with none of the three enabled falls back to
tflint --recursive, so the command never silently does nothing.
secrets scan, policy check and precommit each depend on exactly one tool
and refuse up front when its switch is false, naming the switch and pointing at
devvault build. secrets scan forwards extra arguments to gitleaks:
devvault secrets scan --redact
devvault secrets scan -p kunde-a/network_p --report-format json
For the difference between the container tools and the host tools, and for what
tools.preset seeds, see Configuration.
Customer-wide workflows
devvault customer runs one workflow across every registered project of a
customer, in alphabetical order by project name, printing ==> <customer>/<project>
before each. It stops at the first project that fails.
devvault customer list # the customers in your registry
devvault customer doctor kunde-a # trusted config loads and a backend exists
devvault customer validate kunde-a # the validate chain in every project
devvault customer scan kunde-a # the enabled scanners in every project
devvault customer docs kunde-a # terraform-docs in every project
customer doctor is the only one that starts nothing: it loads each project's
trusted configuration and checks that a container backend is available, which
makes it a cheap way to find a project whose registration or config broke.
customer validate, customer scan and customer docs run in the container and
therefore need every project of that customer to be registered and trusted.
customer scan runs exactly the chain devvault scan
runs, per project and from that project's own tools.* switches, so a customer
whose projects enable different scanners still gets the right ones.
Editor integration
devvault open # current directory
devvault open /path/to/repo
devvault open kunde-a # a registered project
devvault open git@github.com:example/platform.git # clone, prepare, register, open
devvault open --editor vscode
devvault ij # switch between open IntelliJ projects
open accepts a path, a registered project's id or alias, or a Git URL — a Git
URL is cloned and prepared like devvault clone --open. The editor comes from
editor.default in the project's config and can be overridden per call with
--editor: intellij, vscode, cursor, codex or custom. IntelliJ, VS
Code and Cursor are located by their CLI on PATH and, failing that, at their
standard /Applications path. When no editor can be found, open prints the
reason and the path to open manually instead of failing.
devvault ij lists the currently open IntelliJ projects and switches between
them.
--allow-custom-editor. The codex and custom editors are the only ones
whose executable comes from editor.command in the repository's config file.
That file is repository-owned, so honoring it without asking would let a cloned
repository choose a program that devvault starts on your host. devvault therefore
refuses by default:
custom editor uses a configured command; rerun with --allow-custom-editor only for trusted repositories
The opt-in exists on devvault open and devvault clone. Pass it only for
repositories you trust, after looking at editor.command and editor.args. Even
with the opt-in, the command must resolve to an executable file — either on
PATH or as an executable path.
Shell integration
devvault cd prints a registered project's path so a shell can change into it:
cd "$(devvault cd kunde-a)"
cd "$(devvault cd)" # the active project
A shell function makes it a one-word command:
dv() { cd "$(devvault cd "$@")" || return; }
devvault completion generates the completion script:
devvault completion zsh > "$(brew --prefix)/share/zsh/site-functions/_devvault"
devvault completion bash > "$(brew --prefix)/etc/bash_completion.d/devvault"
devvault completion fish > ~/.config/fish/completions/devvault.fish
devvault completion powershell
devvault completion <shell> --help prints the exact installation steps for that
shell, including the one-off compinit line for zsh and the bash-completion
dependency for bash. All four subcommands accept --no-descriptions to leave the
per-flag descriptions out of the generated script. Release archives already
contain the generated scripts under completions/.
Terraform or OpenTofu
OpenTofu is a first-class distribution, not a compatibility mode. It has its own version pin, and the container installs the tofu binary — verified against the release checksums and the OpenTofu GPG signature — plus a terraform symlink pointing at it, because a lot of tooling calls terraform unconditionally.
devvault setup customer --distribution opentofu --customer kunde-a --repo /path/to/repo
devvault setup detects the distribution from the repository when --distribution is not given: a .opentofu-version file or *.tofu sources mean OpenTofu. In the config it is terraform.distribution, and the pinned version comes from terraform.opentofu_version instead of terraform.version.
Every command that runs Terraform uses the configured distribution's CLI name, so
in an OpenTofu project devvault tf plan runs tofu plan and devvault validate
validates with tofu.
Trusted project files
devvault runs files that live in the repository: .devvault/docker-compose.yml decides mounts and privileges, .devvault/Dockerfile decides build steps, and .devvault/scripts/* are executed by editors and CI. devvault generates those files itself, and before it runs anything in a container it checks that they still match what it would generate from your config — or that you approved their current content. The fallback locations a backend would otherwise pick up, ./docker-compose.yml and .devcontainer/Dockerfile, are covered as well, and so is any unexpected entry inside .devvault/scripts/.
A repository that ships its own version of one of these files stops the command with an explanation instead of running it:
devvault trust status -p kunde-a/network_p # per-file state, exit code 1 when untrusted
devvault trust review -p kunde-a/network_p # diff against what devvault would generate
devvault sync -p kunde-a/network_p # regenerate the managed files
devvault trust accept -p kunde-a/network_p # approve the current content on purpose
Approvals are stored in your own registry under ~/.devvault, never in the repository, so a repository can never approve itself. An approval says one thing only: this file has not changed since you agreed to it. It does not say the file still matches your config, and it never expires when the config moves on — trust status reports that separately, and devvault sync is the way back. devvault sync is also how you refresh generated files after a devvault update; --dry-run shows what would change.
Drift in .pre-commit-config.yaml, the CI workflows and .devvault/config.yaml is reported but does not block — those files do not decide what devvault executes.
trust status prints one row per managed file with its state, whether it is
enforced and whether it still matches your config, and ends with
trusted: yes|no. For an unregistered directory it says so explicitly: devvault
does not know which customer and credential set the project belongs to, so it
cannot render the expected content at all, and the differences it shows are
inconclusive rather than evidence of tampering. Register the project first.
FILE STATE ENFORCED CONFIG
.devvault/docker-compose.yml ok yes differs
The STATE and CONFIG columns answer two different questions, and they come
apart after a config change:
STATEis the security verdict: has anything changed since devvault generated the file or since you approved it? Only this decides whether devvault refuses to run something. An approval means "unchanged since you agreed to it" — it does not mean "still matches your config".CONFIGis the consistency one: is the file byte-identical to what devvault would generate from your config right now?match,differs, or-for a file devvault has no template for — a missing file, a fallback such as./docker-compose.yml, or an unexpected entry in.devvault/scripts/.
So ok plus differs is the normal state after devvault tools bump or a
hand-edited .devvault/config.yaml: nobody touched the generated files, they
just fell behind. Nothing is blocked, the exit code stays 0, and trust status
ends with a note pointing at devvault sync, which regenerates them. Leaving it
that way is not a security problem, but it is a real one: the two backends read
the pins from different places — the Apple container backend from the config,
the docker backend from the compose file on disk — so the same project can build
two different images until you sync. trust review lists these files too, marked
approved, no longer matches your config, with the diff devvault sync would
apply.
trust accept approves what is on disk right now, which means devvault will run
those repository-owned files unchanged. It asks for confirmation, and without a
terminal it refuses unless --yes is given. Run trust review first.
devvault sync rewrites the compose file, the Dockerfile, the helper scripts and
the CI files from the config devvault trusts, and records the result as trusted:
devvault sync
devvault sync --dry-run
devvault sync -p kunde-a/network_p --json
Keeping the pinned tool versions current
A project pins exact versions for Terraform or OpenTofu, tflint, terraform-docs, Trivy, OPA, Checkov, pre-commit, gitleaks, conftest and Infracost, and every download in the generated Dockerfile is verified against the checksums published for exactly that version. A pin is kept whether or not the tool is switched on, so enabling one later does not silently install an old release. A stale pin is a security problem, not a cosmetic one:
devvault tools outdated -p kunde-a/network_p # exit code 1 when something is outdated
devvault tools outdated --json
devvault tools bump --dry-run # show the diff without writing
devvault tools bump --only terraform --yes # bump one tool
--only takes one of terraform, opentofu, tflint, terraform-docs,
trivy, opa, checkov, pre-commit, gitleaks, conftest or infracost.
tools outdated reports a tool whose upstream could not be reached as unknown,
with the reason on stderr; that neither fails the command nor hides the other
results.
tools bump rewrites only the version keys in .devvault/config.yaml, keeping comments, key order and every other value, and reloads and validates the result before it returns — if anything but the pins changed, the previous content is restored. It asks before writing, and without a terminal it refuses unless --yes is given, because it edits a file in your repository. Afterwards run devvault sync (the generated Dockerfile and CI files embed those versions), review and re-approve the changed files if the project was approved, and devvault build to rebuild the container.
Updating devvault
devvault update --check # report only; exit code 1 when an update is available
devvault update
devvault update --yes # skip the interactive confirmation
devvault update --force # update even from a dev build, or to a version that is not newer
update downloads the current release from GitHub, always verifies its SHA-256 checksum against that release's checksums.txt, and verifies checksums.txt itself with cosign when cosign is installed — without cosign you get a loud warning that only the checksum was checked. The running binary is then replaced atomically. devvault never checks for updates on its own; this command is the only place it looks. A devvault that Homebrew manages is refused, because replacing it would break that installation; the check follows symlinks, so a binary you installed into /opt/homebrew/bin yourself is still updated.
After updating, run devvault sync in your projects so the generated files match the new devvault.
Exit codes
devvault exits 0 on success and 1 on any error; the error is printed on
stderr. A failing Terraform run inside the container is an error like any other,
so devvault exits 1 and does not forward Terraform's own exit code.
Six commands use the exit code as a result rather than as an error, which makes them scriptable:
| Command | Exit code 1 means |
|---|---|
devvault doctor |
A required host check failed: architecture, git, or "at least one container backend is available". The project registry line is not one of them and never changes this exit code |
devvault registry check |
The registry has at least one finding. Warnings count too: the command exists to report problems, and --json carries the split into errors and warnings for a caller that only cares about one of them |
devvault registry import |
At least one project of the document was not restored. A skipped one counts: it is not registered the way the document describes it. --dry-run reports the same, so it can gate a script before anything is written |
devvault trust status |
The project is not trusted — or it is not registered, in which case devvault cannot determine the expected content at all. A file that only fell behind the config (CONFIG is differs) is a hint, not an error, and keeps the exit code at 0 |
devvault tools outdated |
At least one pinned tool has a newer upstream release |
devvault update --check |
An update is available |
Each of them still prints its full report before exiting, and --json output is
written before the non-zero exit, so a script can read both.
Network access
devvault tools outdated, devvault tools bump and devvault update are the only commands in which devvault itself opens a network connection, and only when you run them explicitly. They use HTTPS and talk to endpoints that are compile-time constants: the HashiCorp releases API, the GitHub API and GitHub releases. GITHUB_TOKEN or GH_TOKEN raises GitHub's anonymous rate limit. There is no background check, no check at startup, and no telemetry.
Other commands can of course still cause traffic through the tools they run — git while cloning, the container backend while building an image, Terraform while talking to a provider, and the AWS CLI whenever a command verifies a profile: credentials status, credentials login, and the guided credentials setup if you answer yes to its closing question.
Configuration
.devvault/config.yaml is generated by devvault init, devvault setup and devvault sync. It is repository-owned and therefore validated strictly: every value that ends up as an argument of a docker or container invocation must match an allowlist, so a value cannot be read as a flag. An invalid value is rejected when the config is loaded rather than passed on.
| Key | Default | Values |
|---|---|---|
backend |
auto |
auto, apple-container, docker; see Choosing a container backend |
project |
directory name, sanitized | identifier |
customer |
same as project |
identifier |
terraform.version |
1.15.8 |
major.minor.patch; checked against the repository's required_version, see below |
terraform.opentofu_version |
1.12.5 |
major.minor.patch, used when the distribution is opentofu; checked the same way |
terraform.distribution |
terraform |
terraform, opentofu |
tools.preset |
standard |
minimal, standard, full |
tools.tflint |
0.64.0 |
major.minor.patch |
tools.terraform_docs |
0.24.0 |
major.minor.patch |
tools.trivy_version |
0.72.0 |
major.minor.patch |
tools.opa_version |
1.18.2 |
major.minor.patch |
tools.checkov_version |
3.3.8 |
major.minor.patch |
tools.precommit_version |
4.6.1 |
major.minor.patch |
tools.gitleaks_version |
8.30.1 |
major.minor.patch |
tools.conftest_version |
0.68.2 |
major.minor.patch |
tools.infracost_version |
0.10.45 |
major.minor.patch |
tools.trivy |
from tools.preset |
container tool: installed into the image only when true |
tools.checkov |
from tools.preset |
container tool: installed into the image only when true |
tools.gitleaks |
from tools.preset |
container tool: installed into the image only when true |
tools.conftest |
from tools.preset |
container tool: installed into the image only when true |
tools.opa |
from tools.preset |
container tool: installed into the image only when true |
tools.precommit |
from tools.preset |
container tool: installed into the image only when true |
tools.graphviz |
from tools.preset |
container tool: installed into the image only when true |
tools.age |
from tools.preset |
container tool: installed into the image only when true |
tools.infracost |
from tools.preset |
container tool: installed into the image only when true |
tools.awscli |
from tools.preset |
host tool: an optional devvault doctor check |
tools.sops |
from tools.preset |
host tool: an optional devvault doctor check |
tools.gpg |
from tools.preset |
host tool: an optional devvault doctor check |
tools.session_manager_plugin |
from tools.preset |
host tool: an optional devvault doctor check |
aws.profile |
<customer>-dev |
letters, digits, ., _, - |
aws.credentials_mount |
~/.aws-kunden/<customer>/<credential-set> |
absolute path or ~/…, no .. segments |
aws.credential_set |
same as project |
identifier |
aws.auth_method |
existing |
sso, assume_role, credential_process, existing, static |
aws.readonly |
true |
mounts the credentials directory read-only |
docker.image_name |
devvault-<project> |
identifier |
docker.container_name |
devvault-<project> |
identifier |
docker.platform |
auto |
auto, linux/amd64, linux/arm64 |
docker.plugin_cache_volume |
devvault-<customer>-tf-cache |
identifier; names the docker volume holding the shared Terraform provider cache, see The shared provider cache |
apple_container.image_name |
devvault-<project> |
identifier |
apple_container.container_name |
devvault-<project> |
identifier; the prefix of the container name — devvault appends a per-run suffix, see Choosing a container backend |
apple_container.plugin_cache_name |
devvault-<customer>-tf-cache |
identifier; names the same cache for the Apple container backend, where it is a directory ~/.devvault/plugin-cache/<name> and not a volume, see The shared provider cache |
editor.default |
intellij |
intellij, vscode, cursor, codex, custom |
editor.command |
empty | only used behind --allow-custom-editor |
editor.args |
empty | only used behind --allow-custom-editor |
ssh.forward_agent |
true |
offer the host SSH agent socket to the container; docker backend only, see Choosing a container backend |
An identifier is lowercase letters, digits, ., _ and -, must start and end alphanumeric, and is at most 64 characters. A version must be exactly major.minor.patch — 1.4 is rejected, 1.4.0 is not. aws.profile follows the same shape as an identifier but also allows uppercase letters.
terraform.version — or terraform.opentofu_version, depending on terraform.distribution — is what the container installs, and it is independent of what your Terraform code demands. Those two can drift apart: a project with required_version = ">= 1.5.0" and a pin of 1.0.11 builds an image that then fails in terraform init. devvault status compares the two and warns when they disagree, and devvault build prints the same warning before it starts building; see Selecting and inspecting projects for the exact output, the constraint syntax devvault understands, and why a constraint it cannot read produces no line at all.
The tool switches fall into two groups, and the group decides where a tool lives.
Container tools — trivy, checkov, gitleaks, conftest, opa, precommit, graphviz, age, infracost — are installed into the generated image exactly when their switch is true. A switch that is false keeps the binary out of the image, and the command that would call it says so instead of failing inside the container.
Host tools — awscli, sops, gpg, session_manager_plugin — are never installed by devvault, because they authenticate you or talk to your own keyring; their switch turns them into an optional check in devvault doctor.
The Terraform or OpenTofu distribution, tflint, terraform-docs and the base packages have no switch: they are what the container exists for and are always installed.
tools.preset seeds those switches when the config is loaded, so it is what decides how large the image gets: minimal enables awscli only, standard adds trivy, gitleaks, graphviz, sops and gpg, and full enables all of them. A switch written explicitly in the same file wins over the preset — and a config generated by devvault init, devvault setup or devvault sync writes every switch explicitly, so changing only preset: in such a file changes nothing. Edit the switches themselves, then run devvault sync (the Dockerfile is generated from them) and devvault build.
The credential-relevant values — aws.credentials_mount, aws.profile and aws.auth_method — are never read from the repository's config file. For a registered project they come from your own registry; for an unregistered one devvault determines none of them and refuses to run anything in the container (see Registered projects). See SECURITY.md for the full trust boundary.
Support
Run devvault doctor to check host tools, backend availability, and project readiness. This project is macOS-first; Linux and Windows hosts are not release targets for v0.1.0.
Documentation
¶
There is no documentation for this package.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
images
Package images owns the host side of the container images devvault builds: which image a registered project is configured to use, which images devvault recorded having built, whether they still exist in the container runtime, what they cost on disk, and what it takes to delete one.
|
Package images owns the host side of the container images devvault builds: which image a registered project is configured to use, which images devvault recorded having built, whether they still exist in the container runtime, what they cost on disk, and what it takes to delete one. |
|
overview
Package overview answers one question across every registered project: which of them need attention.
|
Package overview answers one question across every registered project: which of them need attention. |
|
planfile
Package planfile keeps saved Terraform plans out of the repository and out of other users' reach.
|
Package planfile keeps saved Terraform plans out of the repository and out of other users' reach. |
|
plugincache
Package plugincache owns the host side of the shared Terraform provider cache: where a cache directory lives, how large it is, which registered projects use it, and what it takes to delete one.
|
Package plugincache owns the host side of the shared Terraform provider cache: where a cache directory lives, how large it is, which registered projects use it, and what it takes to delete one. |
|
registrycheck
Package registrycheck inspects the user-owned devvault registry for entries that no longer describe reality.
|
Package registrycheck inspects the user-owned devvault registry for entries that no longer describe reality. |
|
registrytransfer
Package registrytransfer carries the project registrations of a devvault registry from one machine to the next.
|
Package registrytransfer carries the project registrations of a devvault registry from one machine to the next. |
|
selfupdate
Package selfupdate replaces the running devvault binary with the current release from GitHub.
|
Package selfupdate replaces the running devvault binary with the current release from GitHub. |
|
toolversions
Package toolversions compares the tool versions a devvault project pins in its .devvault/config.yaml against the current upstream releases, and rewrites those pins in place.
|
Package toolversions compares the tool versions a devvault project pins in its .devvault/config.yaml against the current upstream releases, and rewrites those pins in place. |
|
trust
Package trust decides whether the devvault-managed files inside a repository may be used to run something on the host.
|
Package trust decides whether the devvault-managed files inside a repository may be used to run something on the host. |
|
version
Package version exposes the build information the running binary was stamped with.
|
Package version exposes the build information the running binary was stamped with. |