Skip to content

Render Reference

dwe render produces files derived from the merged DWE config. It is the single entry point for code-generated artifacts — none of these files should be hand-edited; re-run the corresponding subcommand instead.

CommandOutputSource
dwe render env.env content (stdout or --out <path>)exports.env rules in workspace/defaults.yml + system vars
dwe render idePer-service IDE config files inside each service hubTemplate packs under workspace/templates/ide/<pack>/ driven by manifest.yml
dwe render aiHub-level agent docs (AGENTS.md, CLAUDE.md symlink, …)Template packs under workspace/templates/ai/<pack>/ driven by manifest.yml
dwe render gitPer-service shell git hooks at <svc.Dir>/src/.git/hooks/<basename> (mode 0755)Template packs under workspace/templates/git/<pack>/ driven by manifest.yml
dwe render configPer-service config files (.env, env.php, …) inside each service hub, replaying harvested secretsTemplate packs under workspace/templates/config/<pack>/ driven by manifest.yml

All five subcommands read the same merged config (workspace.ymlworkspace/defaults.ymlworkspace/local.yml, with per-service declarations from workspace/services/<name>/service.yml joined in). They differ in what they iterate and where they write.

flowchart LR
  L1[workspace.yml] --> M
  L2[workspace/defaults.yml] --> M
  L3[workspace/local.yml] --> M
  S["workspace/services/*/service.yml"] --> M
  M[("Merged config")]

  M --> E[render env]
  M --> I[render ide]
  M --> A[render ai]
  M --> G[render git]
  M --> C[render config]

  E --> EOUT[".env / stdout"]
  I --> IOUT["services/{name}/..."]
  A --> AOUT["services/{name}/AGENTS.md<br/>services/{name}/CLAUDE.md<br/>..."]
  G --> GOUT["services/{name}/src/.git/hooks/...<br/>(mode 0755)"]
  C --> COUT["services/{name}/.env<br/>services/{name}/env.php<br/>..."]

Each subcommand:

  1. Loads the merged config. A missing or invalid project config is a hard error.
  2. Selects targets:
    • env — single artifact, no selection.
    • ide / ai / git / config — iterates services, applies a selection policy, and optionally narrows to one service via the [service] argument.
  3. Writes output files. Where they go depends on the subcommand:
    • render ide and render ai write inside each service’s hub directory, anchored to the project root (the directory containing workspace.yml), and enforce path-safety boundaries.
    • render git writes inside <svc.Dir>/src/.git/hooks/ for each service whose src/.git is a real directory; the destination is never tracked by git.
    • render config writes per-service runtime config files (.env, env.php, …) inside each service’s hub directory from workspace/templates/config/<pack>/, replaying harvested secrets; app services are iterated in DeployOrder.
    • render env writes to stdout by default, or to the --out <path> argument as given. The --out path is interpreted relative to the current working directory, not the project root — pass an absolute path if you want a deterministic location regardless of where the command is invoked from.
Aspectrender envrender iderender airender gitrender config
Iterates servicesnoyesyesyesyes (app services, DeployOrder)
Reads templates from disknoyes (manifest-driven)yes (manifest-driven)yes (manifest-driven)yes (manifest-driven)
Per-service opt-in fieldservices.<name>.render.ide.enabledservices.<name>.render.ai.enabledservices.<name>.render.git.enabledresolvable config pack (app-only)
Default opt-in policytrue for type: app; false otherwisetrue for type: app; false otherwisetrue for type: app; false otherwiseapp services only; no pack → silent no-op
Collision policy when services share dirdeepest extends wins (per-variant overrides)shallowest extends wins (canonical hub identity)deepest extends wins (per-variant hooks)extends parent hub rendered once (alias skipped)
Manifest filemanifest.yml declares render (+ symlinks)manifest.yml declares render + symlinksmanifest.yml declares render onlymanifest.yml declares render only
Symlinks supportednoyes (relative, hub-internal)yes (relative, hub-internal)no — to must be a basenameno — rejected
Output moden/aas writtenas writtenexplicit chmod 0755 on every runreplace (overwrite)
Path-safety guardsn/asymlink rejection in pack and destinationsymlink rejection in pack and destinationhub preflight + symlink rejection in .git/hooks/symlink rejection in pack and destination

render ide, render ai, and render git all read a manifest.yml at the root of their chosen template pack using a single shared schema:

render:
- from: <path inside pack, ending in .tmpl>
to: <destination relative to the per-kind dest root>
symlinks:
- link: <symlink path>
to: <existing render destination>

Per-kind constraints layered on top:

KindDest rootto shapesymlinks
ideservice hub directoryany contained relative pathallowed
aiservice hub directoryany contained relative pathallowed, must reference a render to
git<svc.Dir>/src/.git/hooks/basename only (no slashes, no ..)rejected — must be empty

The manifest is loaded with strict YAML decode (yaml.Decoder.KnownFields(true)); unknown fields are a hard error. An empty manifest (no render and no symlinks) is rejected. Validation is split into shape (pure, no filesystem) and sources (resolver-aware existence check) so that shadow-pack overrides participate in source-existence validation identically to how the renderer reads them.

Any template pack workspace/templates/<kind>/<pack>/<rel> can be overridden on a per-file basis by a sibling shadow pack at workspace/templates/<kind>/<pack>.local/<rel>. The resolver applied by all three rendering subcommands is:

  1. Check workspace/templates/<kind>/<pack>.local/<rel>:
    • regular file → use it; the renderer emits one info line using local override: workspace/templates/<kind>/<pack>.local/<rel>.
    • exists but is a directory or symlink → hard error; the override does not silently fall back to the canonical pack (so a bad override surfaces itself).
    • missing → fall through.
  2. Check workspace/templates/<kind>/<pack>/<rel>:
    • regular file → use it.
    • exists but is a directory or symlink → hard error with the offending path named.
    • missing → wrapped os.ErrNotExist.

The <pack>.local/ directory is a sibling of the canonical pack, not a child. It lives inside the tracked workspace/templates/<kind>/ directory and is gitignored by pattern (workspace/templates/*/*.local/ or a broader *.local/ rule — recommend adding to the project .gitignore).

The override pack only needs to contain the files being overridden — it is not a full pack. manifest.yml is read only from the canonical pack; an override cannot rewrite the manifest, just substitute individual from: sources.

This mirrors the existing user-local override convention in the project:

Canonical (tracked)Local sibling (gitignored)
workspace/workspace.ymlworkspace/local.yml (documented in services reference)
workspace/docker.ymlworkspace/docker.local.yml
workspace/templates/<kind>/<pack>/workspace/templates/<kind>/<pack>.local/

.dwe/ (the runtime directory) is never used for user-authored overrides — it is reserved for DWE-managed state (deploy/state.yml, deploy/deploy.lock, logs/).

The override is an input substitution, not an output redirection:

  • The override file at workspace/templates/<kind>/<pack>.local/<rel> is gitignored by the .local/ pattern and never committed.
  • The rendered output still lands at the manifest-declared to.

What that means in practice:

KindOutput pathTracked?Effect of local override
git<svc.Dir>/src/.git/hooks/<basename>never (inside .git/)the override is fully private to the developer
ide / ai<svc.Dir>/<rel> (typically tracked)usually yesre-rendering modifies the tracked artifact; the developer is responsible for not committing those changes (git stash, git checkout -- <path>, or a personal pre-commit guard)

For IDE/AI, a local override that produces a different rendered output is a workflow you opt into deliberately — keep it out of commits the same way you would keep an unrelated WIP edit out.

  • render env.env generation: system variables, export rules, when filtering, value formatting
  • render ide — IDE template packs: pack resolution, manifest schema, deepest-wins collision policy, per-service rendering
  • render ai — Agent docs template packs: manifest schema, shallowest-wins collision policy, render + symlink entries
  • render git — Shell git hooks: manifest-driven hook rendering into <svc.Dir>/src/.git/hooks/, deepest-wins, mode 0755
  • render config — Service config files: ${...} substrate, ${generated.<name>} replay, harvest-not-mint secrets, opt-in pack resolution