devvault-cli

command module
v0.2.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 1 Imported by: 0

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

  • macOS, with Apple Silicon recommended for Apple container.
  • Apple container on 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 let devvault update verify 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 mv and state rm it 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 bump and update it 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:

  • STATE is 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".
  • CONFIG is 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.patch1.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 toolstrivy, 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 toolsawscli, 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

The Go Gopher

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL