Concise notes for contributors working on the enclave codebase.
- Go 1.24.x (see
go.modtoolchain) - Docker daemon running (for runtime testing)
- Linux or macOS host. Native Windows is unsupported; use WSL2, where the Linux
instructions apply.
make cross-buildcompiles every published cross-target:darwin/arm64,darwin/amd64,linux/arm64,windows/amd64, andwindows/arm64. It builds./...rather than just./cmd/enclave, so the windows targets also keep the whole tree portable even though the published windows binary is only the WSL launcher.
Build the CLI (plain make is equivalent to make build):
make buildInstall to your PATH:
make installBuild distribution packages:
make deb
make rpmThe default Enclave image includes both packaging toolchains. On Ubuntu, install
them with sudo apt install debhelper devscripts rpm. On Fedora, install the
RPM build dependencies with sudo dnf install git go make rpm-build. Use
make deb-quick when Go was installed outside APT and therefore does not
satisfy dpkg's build dependency check; it builds the same binary package
without that check.
Run tests:
make testFormat code:
gofmt -w .Lint only changed files/packages (faster than full make lint during iteration):
make lint-changed # staged + unstaged + untracked changes
make lint-changed BASE_REF=main # all changes since mainenclave embeds its runtime assets, but a development binary uses checkout files
first. It locates an "app root" in this order (internal/config/paths.go,
discoverAppRoot):
ENCLAVE_HOME, which must contain the required assets when set.- Walk up from the binary's directory until a valid app root is found.
- Extract the embedded assets into a content-hash-keyed directory under the platform cache root.
On Linux the extraction store is
${XDG_CACHE_HOME:-~/.cache}/enclave/assets/<hash>/. On macOS it is
~/Library/Caches/org.eclipse.enclave/assets/<hash>/. Each distinct asset set
uses its own atomically published directory and does not modify other entries. The tree
is reproducible from the binary, so deleting it is safe; the next run extracts
it again.
Because bin/enclave lives inside the checkout, tier 2 finds the repository
root. Running an in-tree binary therefore uses the working copy's assets:
make build
./bin/enclave --tool <tool> [args…]When in doubt, pin the app root explicitly. This defeats a stale ENCLAVE_HOME,
an installed enclave earlier on your PATH, or launching from an unexpected
directory:
ENCLAVE_HOME="$(pwd)" ./bin/enclave --tool <tool> [args…]Tool templates and other build inputs are baked into the Docker image at build
time, not mounted at run time. The build check hashes asset content into the
image's enclave.hash label, so editing an asset normally triggers an
automatic rebuild. To force it, pass --rebuild:
./bin/enclave --tool <tool> --rebuildGotchas:
- Invoke
./bin/enclave, not an installedenclaveon yourPATH. An installed binary uses the assets embedded when it was built, not your checkout. - Canonical full config overrides under
~/.config/enclave/tools/<tool>/and~/.config/enclave/projects/<hash>/<tool>/config/, plus JSON/TOML patches under the global/projectpatches/<tool>/directories, can shadow your working-copy template. Clear them if you want to see the in-tree version. --no-cacheis not the flag for this — it only disables runtime package cache mounts, notdocker buildcaching or asset refresh. Use--rebuild.
Before committing, run:
make build
make test
make lintException: if a change only touches files under docs/, these make targets are
not required.
Generated files are checked in. If you change the option definitions or tool imports, regenerate and commit the outputs.
Generate all:
make generateOr run directly:
go generate ./internal/config ./cmd/enclaveNotes:
- Option definitions live in
internal/config/options_def.go. - Generated outputs:
internal/config/options_registry_gen.gointernal/config/options_cli_gen.gointernal/model/option_sources_gen.gocmd/enclave/tool_imports.go(tool extension Go imports,//go:build !windows)
On Windows, cmd/enclave builds main_windows.go, a launcher that forwards to
the Linux binary inside WSL2 (see windows.md for the user-facing
behavior). It links neither the embedded runtime assets nor the tool extensions,
because it never builds an image — which is why cmd/enclave/tool_imports.go is
generated with a //go:build !windows constraint. The result is around 3 MB
against roughly 14 MB for the Linux binary.
All the logic lives in internal/wslshim, and everything except the two
drive-letter queries (GetDriveType and WNetGetConnection) is
host-independent, so it builds and is tested on Linux:
go test ./internal/wslshim/...
GOOS=windows go build ./... && GOOS=windows go vet ./...make lint runs go vet and gosec a second time with GOOS=windows, because
neither one looks at a file its build constraints exclude — without that pass the
unsafe.Pointer code in drivetype_windows.go and its #nosec annotations are
checked by nothing. golangci-lint is not repeated that way, so the
Windows-only files remain outside it.
The two shell scripts the launcher sends into the distribution are Go constants,
so internal/wslshim/script_exec_test.go executes them against the host's sh
on non-Windows platforms. That is what keeps probeScript and the marker
parseProbeOutput looks for from drifting apart.
Argument quoting is the risky part. Windows passes a single command-line string
and wsl.exe re-parses it, so the launcher builds that string itself and hands it
to CreateProcess unchanged. Two layers check it:
-
A golden table in
internal/wslshim/quote_test.goround-trips every argument shape through a reimplementation ofCommandLineToArgvW. This runs in CI on every push. The same table is exported tointernal/wslshim/testdata/wsl-quoting-golden.json; regenerate it after changing the escaping or the table, and commit it:ENCLAVE_UPDATE_GOLDEN=1 go test ./internal/wslshim -
scripts/wsl-shim-verify.ps1feeds those same command lines to a realwsl.exeand compares the NUL-separated argv the Linux side receives. This cannot run in GitHub-hosted CI, becausewindows-latestrunners have no WSL2. Run it manually on a Windows host with WSL2 before publishing Windows artifacts:pwsh -File scripts/wsl-shim-verify.ps1
Without PowerShell 7, use Windows PowerShell. A script reached through
\\wsl.localhostcounts as remote, so the execution policy refuses it unless the run bypasses it:powershell -ExecutionPolicy Bypass -File .\scripts\wsl-shim-verify.ps1
- Add fields in
model.Options(and related embedded struct). - Add defaults in
config.Defaultsif configurable. - Add the option entry in
internal/config/options_def.go. - Run code generation (
make generate). - Run tests (
go test ./...). - Update docs for user-visible flag behavior (
docs/cli-reference.md,docs/configuration.md,docs/ARCHITECTURE.md, and related command docs).
For CLI-only options (not configurable via config files), omit DefaultsField
and use Apply: ApplyNone in options_def.go (for example:
--force-base-image and --no-rebuild).
For config-only options, omit CLIFlags instead (for example:
host_config_paths). If a project config must not be able to relax the
option, add it to applyProjectDefaultsGuardrails in internal/config/config.go
alongside the existing guarded keys.
Use this when adding a new option so you do not stop after options_def.go:
- Core types:
add the field to the relevant
model.*Optionsstruct ininternal/model/types.go. - Config-backed options:
add the field to
config.Defaultsininternal/config/config.go. - Removed config keys:
add the key to
removedConfigFieldsininternal/config/config.gosoreadDefaults()rejects it loudly instead of silently ignoring it. - Option registry:
add the definition in
internal/config/options_def.go. - Generated outputs:
run
make generateafter changingoptions_def.go. - Guardrail tests:
if you added a config-backed option or new option source, update the fixture
values in:
internal/config/merge_test.gointernal/model/option_sources_test.go
- Behavioral tests: add or update focused tests for parsing and runtime behavior, not just the generated files.
- Docs:
update
docs/cli-reference.md,docs/configuration.md,docs/ARCHITECTURE.md, and any command docs that describe the affected behavior. - Security-sensitive options:
update
docs/security/README.mdand review whether project config guardrails or validation logic need changes.
Quick rule of thumb:
- CLI-only option: model field,
options_def.go, generate, tests, docs. - Config-backed option: model field,
config.Defaults,options_def.go, generate, merge/source test fixtures, tests, docs. - User-facing runtime option: all of the above plus security notes when applicable.
Backend option note: --backend is config-backed and defaults to auto, which app.resolveBackend turns into docker or podman from backenddocker.DetectCLIs() (prompting once via prompt.Choose and persisting with config.WriteGlobalDefault when both are installed and a terminal is present); podman reuses the Docker backend (internal/backend/docker) over the podman CLI by switching the binary in internal/docker once at startup (docker.SetBinary), with docker.IsPodman() gating the few engine differences (--userns=keep-id, info schema, build cache flags, and renderEngineDockerfile stripping home-directory cache mounts from the rendered Dockerfile); experimental qemu is available for foreground slim/no-feature unrestricted sessions.
Shared skills note: --skills-validation strict|agent is config-backed,
defaults to strict, and supports global, project, and selected tool overrides.
Normalize casing and whitespace before validating; runtime callers resolve the
mode through model.SkillsValidationMode, which falls back to strict for empty
or unknown values. Apply the mode in both skill composition paths. It affects
shared skills only; source precedence and whole-directory replacement remain
unchanged.
Build option note: --features is available on CLI. In devcontainer mode,
unset features default to none, so pass --features explicitly when needed.
--features none is the explicit "no features" selection.
Project mount note: --project-mount writable|readonly is config-backed.
Project config may opt into readonly, but guardrails strip writable at
project scope so project defaults cannot weaken a stricter global setting.
--worktree-metadata follow|readonly|none works the same way for the
linked-worktree gitdir/commondir mounts: project config may strengthen the
inherited mode (follow < readonly < none), but cannot weaken it.
Config additive note: feature additive directives (+/-) are applied against
the implicit default-enabled feature set when features is unset. For example,
["-node-dev"] removes that default feature from the implicit set.
Config additive note: host_config_paths uses the same directive style, but it
resolves against the selected tool profile's reviewed passthroughPaths
instead of global defaults. Use default to include the built-in allow-list
before applying +/- edits.
When a value exists in multiple layers, enclave resolves in this order:
- CLI flags
- Selected tool override (
tool_overrides.<tool>) - Project config (
~/.config/enclave/projects/<hash>/config.json; per-project overrides live outside the worktree, keyed by project hash) - Global config (
~/.config/enclave/config.json) - Built-in defaults
Security guardrail: project config (including tool_overrides) cannot elevate
guarded options such as allow_all_network=true. Those values are ignored with
warnings; use global config or CLI for explicit host opt-in.
- Use
enclave network statusto inspect effective policy and runtime sync state. - Use
enclave network applyto push persisted policy to running gateways. - Mutation commands (
network add-domain/remove-domain/set-mode) auto-apply by default. - Use
--no-applyon mutation commands to persist only. - Use
--all-runningwithnetwork applyor mutation commands to target all running gateways. network set-mode unrestricted --globalis persisted immediately, but running restricted sessions must be restarted; unrestricted mode is not live-applied.
Every extension (tool or feature) must include a README.md in its directory.
See docs/extensions/README.md for the full extension architecture.
Build composition scripts live in runtime-assets/build-scripts/ and are
invoked from Dockerfile.
- Keep
Dockerfilestructural; move feature/tool composition logic into these scripts. - Script contracts are documented in
runtime-assets/build-scripts/README.md. - Validate script changes with
make lint(shellcheckruns on all*.shfiles andbuild-scripts/bin/helpers).
- Keep docs concise and aimed at experienced developers.
- Diagrams are Mermaid code blocks embedded in the docs; update them together with the behavior they document.
Use Conventional Commits for commit subjects:
type: short imperative summary
Allowed types: feat, fix, docs, refactor, test, chore, build, ci,
perf, revert.
Work is tracked via GitHub Issues.