feat(env)!: adopt yaml env overlays and injected session flows#33
Conversation
Replace the legacy .hack/hack.env.json and backend-first project env model with committed hack.env.*.yaml overlays, project-key encrypted secrets, direct runtime injection, host-side env exec/shell helpers, env-aware sessions, and updated docs/templates. BREAKING CHANGE: project env configuration now uses hack.env.default.yaml and optional hack.env.<overlay>.yaml files as the canonical format. .hack/hack.env.json, backend-first secret flows, and .hack/.env as the default runtime source are legacy compatibility paths. Migrate older repos with hack doctor --migrate-env-config and use hack env add/materialize/exec/shell for the new workflow.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d93363408d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 066c4cbb0e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 32dcea0e74
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const cleanupCandidates = [ | ||
| ...migrated.cleanupCandidates, | ||
| ...blockedCleanupCandidates, | ||
| ].sort((left, right) => left.localeCompare(right)); |
There was a problem hiding this comment.
Exclude compose-blocked env files from cleanup removal
This cleanup list currently appends blockedCleanupCandidates even when compose env_file references were not repaired, so accepting the cleanup prompt can delete .hack/.env* files that compose still depends on. In the common path where the user declines the compose repair step, this turns a successful migration into a runtime break (compose will still reference files that were removed). Keep blocked candidates out of the removable set until the compose references are actually cleared.
Useful? React with 👍 / 👎.
| const injectedEnv = | ||
| workspaceProject && projectName | ||
| ? await resolveSessionInjectedEnv({ | ||
| project: workspaceProject, |
There was a problem hiding this comment.
Inject env in session exec only when scope flags are set
hack session exec now resolves and injects env whenever the workspace can be mapped to a project, even if the user did not pass --env/--service. That means commands sent to an already scoped workspace (for example ...env-qa.svc-api) can be overridden by the current default overlay/global scope, which is a silent context switch for migrations or admin commands. Injection should be conditional on explicit scope flags (or inferred from workspace scope) instead of always applying defaults.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1954903dd3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const resolved = await resolveProjectEnvConfig({ | ||
| projectRoot: opts.projectRoot, | ||
| projectDir: opts.projectDir, | ||
| envName: undefined, | ||
| serviceNames, |
There was a problem hiding this comment.
Handle malformed env YAML without aborting project metadata
resolveModernEnvMeta calls resolveProjectEnvConfig directly, and parse errors from .hack/hack.env*.yaml throw here; that rejection bubbles through resolveProjectMeta and buildMetaByProjectName (Promise.all), so a single malformed project env file can make hack projects metadata fail entirely instead of reporting an env parse error for that project. The legacy path preserved this as contractParseError, so this should catch and downgrade parse failures to metadata fields rather than throwing.
Useful? React with 👍 / 👎.
| ? await resolveSessionInjectedEnv({ | ||
| project: projectContext, | ||
| projectName: baseName, | ||
| envName: undefined, | ||
| serviceName: null, |
There was a problem hiding this comment.
Preserve scoped env when cloning an attached workspace
In the interactive picker path for an attached workspace, selecting “Create isolated” keeps the scoped workspace name (resolveNextIsolatedWorkspaceName on the original scoped name) but injects env using envName: undefined and serviceName: null, so the new workspace can run with default/global env while its name still advertises a scoped env/service. This mismatch can silently run commands against the wrong environment when users branch from a scoped workspace.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2c6b5dc53e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| `environment: ${environment}`, | ||
| "secretsprovider: project_key", | ||
| "values:", | ||
| " global:", |
There was a problem hiding this comment.
Emit an object for default global scope in env template
renderProjectEnvConfigYaml() writes values.global as global: with no mapping, which YAML parses as null. New projects created via hack init then produce an invalid hack.env.default.yaml, and subsequent modern env operations (for example hack env add ...) fail parse validation (Env scope "global" must be an object). This blocks the primary env workflow immediately after initialization.
Useful? React with 👍 / 👎.
| (args.options.env !== undefined || serviceName !== null) && | ||
| !(workspaceProject && projectName) |
There was a problem hiding this comment.
Treat explicit --service global as env injection request
In session exec, the unresolved-project guard checks serviceName !== null, but resolveRequestedServiceName() normalizes --service global to null. If a workspace cannot be mapped to a project, hack session exec --service global ... bypasses the error path and still runs the command without any injected env, silently ignoring an explicit scope flag. This should key off args.options.service !== undefined (like --env) rather than the normalized value.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dd649b7cd3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if ( | ||
| (args.options.env !== undefined || serviceName !== null) && | ||
| !(workspaceProject && projectName) |
There was a problem hiding this comment.
Reject scoped exec when workspace project cannot be resolved
When hack session exec is run against a scoped workspace name (for example project.env-qa.svc-api) but the project context cannot be resolved (stale registry entry, running outside repo, etc.), this guard does not fire because it only checks explicit flags. The command then proceeds with env: undefined, silently dropping the inferred scoped env injection and running with ambient host env instead. For migration/admin commands this can target the wrong environment without an error; the unresolved-project check should also trigger when inferred scope selection requires injection.
Useful? React with 👍 / 👎.
## 2.0.0 (2026-03-27) * fix(build): stabilize macOS release signing ([32dcea0](32dcea0)) * fix(ci): stabilize env migration branch checks ([498d793](498d793)) * fix(doctor): group checks and hide noisy follow-up ([e6f6ace](e6f6ace)) * fix(doctor): make env migration prompts explicit ([b000438](b000438)) * fix(doctor): repair compose env refs in modern projects ([1954903](1954903)) * fix(doctor): repair legacy env cleanup and tickets retry ([1685328](1685328)) * fix(env): clean legacy config during migration ([eac34d6](eac34d6)) * fix(env): keep app and project metadata compatible ([7d6fe17](7d6fe17)) * fix(env): keep init templates and session scope strict ([aee89ba](aee89ba)) * fix(env): store canonical env configs under .hack ([f4ff398](f4ff398)) * fix(env): support CI key fallback and mask modern secrets ([85e88f8](85e88f8)) * fix(env): tighten scope validation for modern overlays ([27dd7c1](27dd7c1)) * fix(runtime): degrade gracefully when docker is unavailable ([ad0d2f9](ad0d2f9)) * fix(session): preserve scoped env injection semantics ([64e46e1](64e46e1)) * fix(test): account for runtime-unavailable offline status ([0a0e5ab](0a0e5ab)) * fix(test): isolate offline optionality matrix ([c93ae8a](c93ae8a)) * fix(test): isolate session env suite from Bun mock bleed ([f7c9a2d](f7c9a2d)) * fix(tickets): force fresh remote checkout during repair ([a58cff4](a58cff4)) * fix(tickets): isolate hidden ref fetches from remote refmap ([dd649b7](dd649b7)) * fix(tickets): repair against refreshed hidden refs ([c1c2498](c1c2498)) * fix(tickets): retry non-fast-forward repair pushes ([2c6b5dc](2c6b5dc)) * feat(env)!: adopt project env config overlays ([a986dfd](a986dfd)) * feat(env)!: adopt yaml env overlays and injected session flows ([d933634](d933634)) * Merge branch 'main' into feat/pulumi-style-env-config ([066c4cb](066c4cb)) * Merge pull request #33 from hack-dance/feat/pulumi-style-env-config ([b064522](b064522)), closes [#33](#33) * feat(env): redesign portability around Pulumi-style config ([02228a2](02228a2)) * feat(env): refine interactive config and key bootstrapping ([bd99f6e](bd99f6e)) ### BREAKING CHANGE * project env configuration now uses hack.env.default.yaml and optional hack.env.<overlay>.yaml files as the canonical format. .hack/hack.env.json, backend-first secret flows, and .hack/.env as the default runtime source are legacy compatibility paths. Migrate older repos with hack doctor --migrate-env-config and use hack env add/materialize/exec/shell for the new workflow.
Summary
hack.env.default.yamland optionalhack.env.<overlay>.yamlhack env materializefor compatibility onlyhack env add,hack env exec,hack env shell, env-aware session flows, and CI fallback viaHACK_ENV_SECRET_KEYBreaking change
This PR changes the canonical project env format.
Repos should migrate from legacy
.hack/hack.env.jsonand backend-first env storage to the new YAML overlay model:hack.env.default.yamlhack.env.<overlay>.yaml.hack.secret.keyorHACK_ENV_SECRET_KEYUse:
Verification
bun x ultracite check src/mcp/agent-docs.ts src/commands/env.ts src/commands/session.ts src/lib/hack-env.ts src/lib/project-env-config.ts src/mux/mux-backend.ts src/mux/tmux-backend.ts src/mux/zellij-backend.ts tests/session-help.test.ts tests/env-exec-command.test.ts tests/session-env-command.test.tsgit diff --check