Repository navigation
Conversation
Add `hack env hydrate`, which reads KEY=value entries (or one JSON object with --format json) from piped stdin or --file and writes them to the machine-local env layer in one rename-committed write. - Only the effective local layer is written; a git-tracked one is refused. - An entry that already holds its value is kept (secret ciphertext and scalar YAML type), and a no-op import does not rewrite the file. A changed value gets new ciphertext with a fresh IV. - Local entries outside the input, other scopes and removal markers for other keys are kept. - A secret in the layer that does not open under the reader key, a key-file mint that would shadow HACK_ENV_SECRET_KEY, a held or stale hydration lock, and a layer changed by another writer (compare-and-swap recheck before the rename) all refuse without changing the layer. - Input is bounded (1 MiB), strict and all-or-nothing; refusals are closed and value-free and name at most a line number. - Unexpected positional arguments are now reported by count, not echoed, so `hack env hydrate KEY=value` cannot print the value. Refs HACK-1158. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gitleaks flagged the literal HACK_ENV_SECRET_KEY fixture as a generic API key. Build it at runtime like the repo's other key-shaped fixtures and record the fingerprint of the branch commit that still holds the literal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Refuse hydration as tracking_unknown when the project is inside a git checkout but git cannot report whether it tracks the local layer (exit 128 from a safe.directory refusal or damaged index, or git missing). Outside any checkout, hydration still proceeds. - Refuse a plain import (no --secret) of a key the target scope stores as a secret (secret_downgrade), instead of silently replacing ciphertext with plaintext. - Refuse repeated top-level keys in --format json input (duplicate_key), matching the env format, instead of keeping the last value. - Pin the unreadable_layer refusal (invalid YAML, wrong shape, wrong version) and the key-file-before-HACK_ENV_SECRET_KEY reader priority with regression tests; share one runtime-built synthetic key helper. - docs/env.md lists every hydration refusal. Refs: HACK-1158 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Resolve src/lib/fs.ts: keep F7's target inspection, which refuses an unreadable, non-regular, unwritable or unownable target instead of reading it as absent, and apply F8's expectedExisting compare-and-swap to the inspected text. The pre-rename check also uses the inspection, so a target that became unreadable is refused rather than matching an expected absence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The F7 branch now includes next, which switched one private-file write in project-env-config.ts to publishPrivateFileExclusive and dropped the createPrivateFileExclusive import. This branch's hydration lock still creates its lock file exclusively, so import both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve generated plugin conflicts (plugins/hack/skills/hack-cli/references/patterns.md ) by regenerating them with bun run generate:agent-plugins from the merged canonical source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear: HACK-1158 (WU27-HYDRATE). Stacked on #340 (
wf/f7-stable-ciphertext-b16), which provides semantic ciphertext reuse and the rename-committed layer write. Review and merge #340 first.Final behavior
hack env hydrateimports many values into the machine-local env layer in one commit:--file(regular files and pipes such as<(producer)). Input is bounded at 1 MiB. Theenvformat is strict (KEY=value, comments,export,'literal',"escaped").--format jsontakes one object of non-empty strings. Repeated keys are refused in both formats; JSON input is not resolved last-wins. One malformed entry refuses the whole input. Messages name at most a line number.hack.env.local.yamlorhack.env.<overlay>.local.yaml). Tracked defaults are untouched. A git-tracked local layer is refused. Inside a git checkout where git cannot say whether the layer is tracked (exit 128 from asafe.directoryrefusal or a damaged index, or git missing), hydration also refuses, so the tracked check fails closed. Outside any checkout it proceeds.secret_downgrade) rather than silently replacing ciphertext with plaintext.ClosedCliError):tracked_layer: git tracks the target local layer.tracking_unknown: inside a git checkout, git could not report whether the layer is tracked.unreadable_layer: the local layer exists but is not a valid env file (invalid YAML, wrong shape or version). It is never replaced.wrong_key: a secret already in the layer does not decrypt under the reader key, or no key exists. The key file is checked beforeHACK_ENV_SECRET_KEY, as readers do. No key is minted.key_env_only:--secretwith onlyHACK_ENV_SECRET_KEYand no key file, because minting a file would replace that key for every layer.secret_downgrade: a plain import of a key stored as a secret in the target scope.locked/stale_lock: hydrations are serialized by.hack/.internal/env-hydrate.lock(exclusive create, holds the PID). A lock from an exited process is reported with its path, never taken over.concurrent_write: compare-and-swap. The layer must still equal the bytes read before staging and again after fsync, just before the rename.--jsonoutput list only added, updated and unchanged keys.Unexpected argumentsnow reports a count instead of echoing the arguments, sohack env hydrate API_TOKEN=valuecannot print the value. This applies to every command.Docs:
docs/env.mdgets a hydration section that lists every refusal, plus a matrix row.docs/reference/cli.mdis regenerated. Pattern card HACK-P08 listshack env hydrate --file <path> --secretand its remaining gap, re-rendered intoplugins/hack/skills/hack-cli/references/patterns.md.Rework after verification (head
fba3e87c)HACK_ENV_SECRET_KEYat runtime and adds the fingerprint of the old line in 06f2623 to.gitleaksignore. CI scans the PR's full history, so the runtime change alone would not have passed. fba3e87 moves that test onto a shared runtime-built key helper.unreadable_layerrefusal (invalid YAML, a non-mapping scope, a wrong version, in secret and plain mode, bytes unchanged) and the reader-key priority (key file plus a differentHACK_ENV_SECRET_KEY: plain and secret hydration succeed, and readers open every entry).tracking_unknownrefusal, described above.secret_downgraderefusal, described above.duplicate_key, including escaped spellings such asAPI_TOKEN. Nested keys and quotes, commas or brackets inside values are not miscounted.docs/env.mdis complete, includingunreadable_layerandstale_lock.Validation (local, macOS host, head
fba3e87c)tests/project-env-hydrate.test.ts,tests/env-hydrate-input.test.ts,tests/cli-command.test.ts,tests/integration-ownership.test.ts,tests/cli-spawn-env-isolation.test.ts: 48 pass, 0 fail.a whitespace-only key file is refused). The rerun passed.bun run --cwd packages/cli typecheck: clean.bun run --cwd packages/cli check: 0 errors (34 existing warnings). Biome on every changed file: clean.--log-opts='--full-history --diff-filter=tuxdb HEAD': 915 commits, no leaks.Negative controls
Every new test was proven with a mutation. Mutants killed:
unreadable_layerguard removedenvironmentKey ?? fileKey)tracking_unknowncheck removedsecret_downgradeguard removedJSON scanner mutants, also all killed:
Two mutants survived and were found to be equivalent: a redundant depth check and a nested-open reset. Both were removed from the code. The original change's 17 mutants were not rerun; the code they target did not change.
A secret-scan negative control used the base branch's config with no fingerprint: the PR range still reports 1 leak, from 06f2623. The current test file scans clean on its own.
Tests use synthetic values, private HOME via
privateCliEnv, and temp git repos. Thetracking_unknownfixture corrupts the temp repo's own.git/index. No test touches Docker, the keychain or real config.For the reviewer
--localoverride of that same key. This is disclosed in the HACK-P08missingContractand in the docs, and the pattern card stayspartial. Please confirm whether this meets the bar.secret_downgradeis a new refusal with no override flag. The way out is--secret, orhack env unset <key> --localand then a plain import.hack env addwithout--secretstill downgrades, as before.tracking_unknownrefuses on a machine without git when the project has a.gitentry. This is deliberate: fail closed.Remaining gaps
hack env set --localdoes not take the hydration lock. Only the pre-rename recheck guards against it, which narrows the window but does not close it.--envomitted writes the base local layer, ashack env adddoes today, not the configured default overlay.fba3e87cis pending.🤖 Generated with Claude Code