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
- 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.bundle:
- 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.bundle \
--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
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>.
A Homebrew cask is prepared in .goreleaser.yml but the tap Marcel2409Dev/homebrew-tap does not exist yet. Once it does, brew install --cask becomes an option; until then the download above is the supported way to install.
To update an installed binary, use devvault update or replace it with the binary from a newer release. To uninstall, remove the installed devvault binary.
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 |
backend doctor |
Runs terraform init -backend=false to check backend configuration |
build |
Builds the project's development container image |
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 |
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 |
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 |
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, context, credentials login, credentials status,
destroy, docs, fix, graph, 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
context, credentials list, credentials status, current, doctor, fix,
list, status, sync, tools outdated, trust status and version.
credentials status --json moves the AWS CLI's own output to stderr so stdout
carries exactly the one document devvault promises.
--yes confirms an operation devvault will not perform unattended. It has
two 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. - On
add,trust accept,tools bumpandupdateit replaces an interactive confirmation. Without a terminal those commands refuse rather than assume consent; the reasons are in Registered projects, Trusted project files, Keeping the pinned tool versions current and Updating devvault.
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 and devvault quickstart — 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.
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.
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
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 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 "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.
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.
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> |
--auth |
Auth method |
--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 |
When --customer and --set are both given, the credential set is also recorded
in your registry, which is what makes it available to projects.
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.
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 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.
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. To start over with an empty cache, remove it —
rm -rf ~/.devvault/plugin-cache/devvault-kunde-a-tf-cache or
docker volume rm devvault-kunde-a-tf-cache — and the next devvault tf init
fills it again.
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 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.
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.
Four 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" |
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.
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 |
terraform.opentofu_version |
1.12.5 |
major.minor.patch, used when the distribution is opentofu |
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 |
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.
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
|
|
|
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. |
|
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. |