Repository navigation
Conversation
`new EnvGraph({ mode: 'shape' })` builds a graph for tools that only
inspect a schema (type checkers, language servers, CI linters):
- only resolvers marked `pure: true` run, all others resolve to undefined
- an item whose value depends on a skipped resolver, directly or through
references, is unknown: it is left unset and is not coerced, validated or
reported as a missing or failed value
- a @disable or @import condition that is unknown keeps the file enabled
- root decorator `execute()` is skipped (the items they reference are still
resolved, so they keep their final validation state)
- no cache store is created
- plugins that are not installed or cached are never downloaded
- process.env overrides are ignored
Skipped work is recorded in `graph.shapeSkipped`. Default mode is
unchanged. The built-in resolvers that only read other values (concat, ref,
fallback, forEnv, if, ...) and the builtin VARLOCK_* vars are marked pure;
VARLOCK_BRANCH skips its local `git branch` lookup in shape mode.
Documents shape mode for tool authors, including the `pure` flag for plugin resolvers.
- builtin variables no longer read the real shell env (an env passed in explicitly is still used), so CI variables cannot change results - generated code is only written inside the root schema's directory; a path that leaves it (.., absolute, or through a symlink) is a schema error - an import that is not a regular file (a named pipe) is a loading error instead of a read that never returns - guide: documents these, that plugin pure flags are trusted like the plugin code that already runs, and that schema regexes have no timeout Tests for each fail on the previous commit.
…known parts - the named-pipe check moves from the direct-import branch to FileBasedDataSource, so a pipe found through a directory import, or the root schema itself, is refused too instead of hanging the load - an error is only treated as caused by a partial value when the resolver it was first thrown from had an unknown input, so an error from a fully known part of the same expression is still reported - the CI-branch test also checks that git is not run Tests for the pipe cases and the mixed expression fail on the previous commit.
- ConfigItem keeps the unknown flag computed when its value resolver ran, so each dependent read no longer walks the resolver tree - FileBasedDataSource does one stat instead of an access check plus a stat
…lk values A file kept only because its @import enabled= or @disable condition could not be evaluated might be skipped entirely by a normal load, so a @plugin in it (or in a file it imports) is no longer loaded. @setValuesBulk now runs in shape mode: it only resolves its args and edits the graph. Data written in the schema is applied as in a normal load. When the data or its enabled= condition comes from a skipped resolver, the items it could set are unknown while empty instead of failing the required check. A local plugin whose package.json is a named pipe or device is refused in both modes instead of blocking the read forever.
… project folder In shape mode the loader does not clear or open a cache store.
… on a skipped value as unknown A computed @required/@optional or @sensitive/@public built from a skipped resolver is now unknown instead of whatever the empty value happened to give: the item is treated as optional and sensitive, and isRequiredUnknownInShapeLoad / isSensitiveUnknownInShapeLoad say so. forEnv() with an unknown @currentEnv is unknown instead of an error, so a @disable or @required built on it no longer fails the load. Checks that only hold for a sensitive item are skipped while sensitivity is unknown. Default mode and generated types are unchanged.
finishLoad returned before the cycle check when any item had a schema error. An item on a cycle was then left unmarked, waited on itself when values were resolved, and the CLI gave up with "varlock stopped before the command finished" instead of showing either error.
…ith a generated soundness test A shape load now reports an item as unknown, instead of as valid, when one of its dependencies is unknown (a normal load fails an item whenever a dependency is invalid, read or not), when it is empty and its requiredness is unknown, or when its sensitivity is unknown and a sensitive-only check would fail. shape-soundness.test.ts generates 1,200 schemas and checks, for every item, that a shape load says unknown or gives exactly the value and errors of a normal load, and the same required and sensitive unless flagged unknown. It fails on every seed without these rules.
…d list; classify every built-in resolver A named pipe or device is skipped and recorded (kind nonRegularFile) instead of failing the load: 1Password mounts a .env that way. It could set any item, so every value is then unknown. shapeSkipped no longer lists root decorators that have no execute(). A test lists every built-in resolver as pure or skipped, so a new one fails until someone decides. Also: resolution-context is imported once, and errors from unknown inputs are tracked in a WeakSet instead of a property written onto the error.
theoephraim
force-pushed
the
shape-mode-v2
branch
from
November 13, 2026 00:47
6c636bb to
c3c443d
Compare
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.
why
opening a repo has turned into a way to get attacked. from the last three months:
.vscode/tasks.jsonand a.claude/settings.jsoninto every repo it can reach, so "a developer can still be infected by just opening the repository"a
.env.schemacan run a command, call a secret store and download a plugin, and today the only way to learn what a schema says is to do all of that. the VS Code extension is safe only because it doesn't use varlock's loader at all. the shared core we talked about in #469 would use it, and so dovarlock codegenand a type checker. shape mode is the load those can use on a folder someone just opened.what a shape load never does
runs
exec()or any resolver not markedpure, executes a root decorator (except@setValuesBulkon data written in the schema), opens the cache, downloads a plugin, reads the shell env, reads a named pipe.the promise
for every item it says unknown, or exactly the value and errors a normal load gives. same for required and sensitive, unless that one is flagged unknown.
shape-soundness.test.tsgenerates 1,200 schemas and loads each both ways. it fails on every seed if the unknown rules are removedwhat's left: plugins
of the 13 hostile schemas that aren't clean, one is a regex written to backtrack forever, which the calling program can defuse for itself (the mapper does). the other 12 are all the same thing: a plugin that is installed still runs its own code when loaded. you described the fix in #469 back in march:
that was a while ago, but i think it's the right answer, and this is where it plugs in: a plugin with a manifest never has to be imported in a shape load. i have a rough version for the 18 first-party plugins. not in this PR.
the small PRs came out of this
#1282 to #1288 were all found while building and testing shape mode: the fuzzer, the hostile schemas and the generated test above each turned up bugs that are also in a normal load.
other limits
forEnv, bulk values or pluginsenabled=condition keeps its items as if it surely existsyour call
the name
shape, whetherpureshould be public plugin API yet, and whethershapeSkippedis public or internal.includes the fix from #1286 so the generated test can't hang. i'll drop it once that merges.