A terragrunt replacement, in TypeScript
Run Terragrunt configurations. Or read them yourself.
Substitute tghclp for the terragrunt command and your configurations run unchanged. Because it is a library first, the same parser, evaluator and workspace graph are importable — so a configuration is something your own tooling can read, not only something a binary executes.
shell
npm install -g tghclparser
tghclp init
tghclp plan
tghclp apply --all
import { Workspace } from 'tghclparser'
ReplaceThe same commands
init, plan, apply, destroy, stacks, hooks and backend bootstrap. run-all is spelled --all; nothing else about a configuration changes.
ImportA library, not just a binary
The parser, evaluator and workspace graph are exported. Read a configuration from your own code instead of shelling out and parsing text back.
InstallWherever Node runs
One npm dependency, no Go toolchain and no platform binary to match. The same package serves CI, an editor extension and an application.
CheckBehaviour compared, not assumed
Compatibility is established by running the same configuration through both executables and comparing what each produces. A difference is a bug here.
What is here
How-toReplace terragrunt
Install, substitute the command, and the one migration hazard worth knowing.
ReferenceCommand line
Every command, the terragrunt equivalent of each, and the options they take.
TutorialParse your first configuration
Ten minutes: parse a file, inspect its tokens, read diagnostics.
ReferenceJavaScript API
The classes, methods and types the package exports.
How-toValidate in CI
Fail a build on diagnostics, and keep the machine-readable results.
How-toBuild a workspace graph
Follow includes, dependencies, reads and stack components.
How-toAdd language features to an editor
Diagnostics, completions, hovers and document links.
ReferenceSupported files
Which files are recognised, and the language regime version 1 follows.
ExplanationHow the toolkit fits together
Why syntax, schema, workspace context and evaluation stay separate.
ExplanationTrust and evaluation
What evaluation may read, and the boundary it will not cross.
Tutorial
Parse your first Terragrunt configuration
In about ten minutes, you will create a small TypeScript program that parses HCL, inspects its token tree, and prints diagnostics.
Level BeginnerTime 10 minutesResult A working parser script
Before you beginUse a supported Node.js release and a project where TypeScript can run. The examples use tsx, but any ESM-capable TypeScript runner works.
-
Create a project
Start in an empty directory and install the parser, its TypeScript peer, and a development runner.
npm init -y
npm install tghclparser typescript
npm install --save-dev tsx
-
Add a Terragrunt file
Create terragrunt.hcl. This small configuration is valid enough to show structure and references.
locals {
environment = "development"
}
inputs = {
environment = local.environment
replicas = 2
}
-
Parse the document
Create inspect.ts. A document belongs to a workspace, and both use file URIs rather than plain paths.
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { ParsedDocument, Workspace } from 'tghclparser';
const filePath = resolve('terragrunt.hcl');
const uri = pathToFileURL(filePath).toString();
const content = await readFile(filePath, 'utf8');
const workspace = new Workspace();
workspace.setWorkspaceRoot(pathToFileURL(process.cwd()).toString());
const document = new ParsedDocument(workspace, uri, content);
await workspace.addDocument(document);
console.log('diagnostics:', document.getDiagnostics());
console.log('root tokens:', document.getTokens()[0]?.children.length ?? 0);
-
Run it
You should see an empty diagnostics array and a non-zero root-token count. You have parsed real HCL and attached it to workspace context.
-
Inspect the token tree
Add this traversal after the existing output. Each token records its type, display value, source range, parent, and children.
function printToken(token, depth = 0) {
console.log(`${' '.repeat(depth)}${token.type}: ${token.getDisplayText()}`);
for (const child of token.children) printToken(child, depth + 1);
}
for (const token of document.getTokens()) printToken(token);
-
Introduce an error
Remove the closing brace from the inputs object and run the script again. The diagnostics now contain source-positioned feedback. Restore the brace when you are done.
What you learned
You created a Workspace, parsed content into a ParsedDocument, read syntax and schema diagnostics, and traversed the token tree. Those same objects underpin completions, hovers, links, dependency graphs, and evaluation.
How-to guide
Replace terragrunt with tghclp
Run your existing Terragrunt configurations through tghclp instead, without changing them.
Install
npm install -g tghclparser
The package installs one executable, tghclp. It needs OpenTofu or Terraform on the path, the same as terragrunt does.
Substitute the command
Configurations are unchanged. Only the executable differs.
terragrunt init
terragrunt plan
terragrunt run-all apply
tghclp init
tghclp plan
tghclp apply --all
Note the last line: where terragrunt spells the whole-graph run run-all <command>, this spells it <command> --all.
Check a configuration before you rely on it
Validation is a separate command, so a configuration can be checked without running anything:
tghclp hcl validate --working-dir ./infrastructure
Where a configuration reads a file outside the workspace root — a deployment plan in a sibling checkout, say — name that directory. Terragrunt has no such boundary and will not have needed this.
tghclp --allow-path /srv/plans plan
Do not share a directory mid-migration
Each tool signs the files its generate blocks write, and refuses to overwrite a file bearing the other's signature. That protects hand-written files, but it means a working directory already generated into by terragrunt will refuse a tghclp run until those files are removed:
rm -f gen_*.tf gen_*.tfvars
tghclp init
Delete only files a generate block produces. Anything a person wrote is refused for the same reason and should stay.
Where behaviour is established
Compatibility is checked by running the same configuration through both executables and comparing what each produces — the generated files, the evaluated values, the refusals — rather than by reading the documentation and assuming agreement. A difference is a bug here, and worth reporting.
How-to guide
Validate Terragrunt HCL in CI
Fail a continuous-integration job when the repository contains syntax or schema diagnostics, and preserve machine-readable results.
Install and run validation
Install tghclparser as a development dependency so the validator version is pinned by your lockfile.
npm install --save-dev tghclparser
npx tghclp hcl validate --working-dir ./infrastructure
The command recursively discovers supported Terragrunt HCL files. It exits with status 1 when diagnostics exist and 0 when validation passes.
Add a GitHub Actions job
name: Validate Terragrunt HCL
on:
pull_request:
push:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx tghclp hcl validate --working-dir ./infrastructure
Capture JSON diagnostics
Use --json when another tool will consume the result. Output is a JSON array only when diagnostics are present; always use the process exit status as the pass/fail signal.
npx tghclp hcl validate \
--json \
--working-dir ./infrastructure \
> diagnostics.json
List only affected configurations
npx tghclp hcl validate \
--show-config-path \
--working-dir ./infrastructure
Experimental syntaxLanguage features behind an experiment gate must be enabled explicitly with --experiment <name>. For example, use --experiment deep-merge only where that feature is intentional.
CI checklist
- Pin the package through
package-lock.json.
- Point
--working-dir at the repository subtree you intend to validate.
- Treat the exit status—not the presence of standard output—as authoritative.
- Upload JSON output as an artifact only when a later job needs it.
How-to guide
Build a workspace dependency graph
Discover the relationships between includes, dependencies, reads, and generated stack components.
Print DOT from the command line
For visualization tools such as Graphviz, generate a DOT graph directly.
npx tghclp dag graph --working-dir ./infrastructure > graph.dot
dot -Tsvg graph.dot -o graph.svg
To graph discovered configurations through list, enable dependency edges explicitly:
npx tghclp list \
--format=dot \
--dependencies \
--working-dir ./infrastructure
Build the graph in TypeScript
import { pathToFileURL } from 'node:url';
import { Workspace } from 'tghclparser';
const workspace = new Workspace();
workspace.setWorkspaceRoot(pathToFileURL('/repo/infrastructure').toString());
const root = await workspace.refreshDependencyTree();
await root?.breadthFirstTraversal(async (node, depth) => {
console.log(`${' '.repeat(depth)}${node.type}: ${node.name}`);
return true;
});
Query one configuration
Use a file URI when asking for dependencies or dependents.
const unitUri = pathToFileURL(
'/repo/infrastructure/app/terragrunt.hcl'
).toString();
const dependencies = await workspace.getDependencies(unitUri);
const dependents = await workspace.getDependents(unitUri);
console.log(dependencies.map((config) => config.targetPath));
console.log(dependents.map((config) => config.targetPath));
What counts as an edge?The graph records include relationships, dependency blocks, dependencies.paths, files consumed by read_terragrunt_config, and explicit unit or stack targets.
How-to guide
Add language features to an editor
Turn an open Terragrunt file into diagnostics, completions, hover content, and navigable document links.
Create and register a document
import { pathToFileURL } from 'node:url';
import { ParsedDocument, Workspace } from 'tghclparser';
const rootUri = pathToFileURL('/repo/infrastructure').toString();
const fileUri = pathToFileURL('/repo/infrastructure/app/terragrunt.hcl').toString();
const workspace = new Workspace();
workspace.setWorkspaceRoot(rootUri);
const document = new ParsedDocument(workspace, fileUri, sourceText);
await workspace.addDocument(document);
Read language-service results
Positions are zero-based LSP positions: the first line and first character are both 0.
const position = { line: 8, character: 17 };
const diagnostics = document.getDiagnostics();
const completions = await document.getCompletionsAtPosition(position);
const hover = await document.getHoverInfo(position);
const links = await document.getLinks();
const token = document.findTokenAtPosition(position);
Handle edits
Update the document content, then re-add it so the workspace rebuilds relationships owned by that document.
document.setContent(updatedSourceText);
await workspace.addDocument(document);
publishDiagnostics(fileUri, document.getDiagnostics());
Handle close events
workspace.removeDocument(fileUri);
Protocol typesDiagnostics, completions, hovers, positions, and links use the types from vscode-languageserver. You can pass them directly through an LSP implementation or adapt them for another editor protocol.
Reference
Command line
The tghclp executable validates, discovers, formats, evaluates, scaffolds, and orchestrates Terragrunt configurations. It is a drop-in replacement for the terragrunt command.
Synopsis
tghclp [global options] <command> [options]
Terragrunt equivalents
Each command below does what the terragrunt command beside it does. Substituting one executable for the other requires no change to a configuration.
| tghclp | terragrunt |
| tghclp init | terragrunt init |
| tghclp plan | terragrunt plan |
| tghclp apply | terragrunt apply |
| tghclp apply --all | terragrunt run-all apply |
| tghclp run -- <command> | terragrunt run -- <command> |
| tghclp hcl validate | terragrunt hcl validate |
| tghclp stack generate | terragrunt stack generate |
| Command | Purpose |
| hcl validate | Recursively validate supported HCL and JSON configurations. |
| hcl format | Format HCL with the selected OpenTofu or Terraform binary. |
| find, fd | Discover unit and stack configurations. |
| list, ls | List configurations as text, a tree, or DOT. |
| dag graph | Print the resolved configuration graph in DOT form. |
| render | Evaluate and print a configuration as JSON. Dependency outputs that are absent or could not be read, because the output command failed or returned no output map, leave affected fields out; stderr names them with the reason and exit status 2 marks the JSON incomplete. Invalid configuration exits 1 without JSON. |
| info print | Print evaluation-context paths and command details. |
| run | Validate, run hooks, then invoke OpenTofu or Terraform without a shell. |
| exec | Execute an external command directly without a shell. |
| stack generate | Generate explicit stack component targets. |
| stack run | Run a command over generated stack units. |
| stack output | Collect output from generated stack units. |
| stack clean | Remove generated stack material. |
| scaffold | Create a unit configuration for a local module. |
| catalog | Print configured catalog entries as JSON Lines. |
| backend | Bootstrap, delete, or migrate backend state. |
| inspect | Print what the language service holds for a configuration as JSON. A registry module source (tfr://) is fetched first, with the ambient registry credentials, so its variables are shown. |
| cache clear | Delete the remote modules fetched into the per-user cache, so the next use fetches them again. |
Validation options
| Option | Meaning |
| --working-dir <path> | Resolve discovery and relative paths from this directory. |
| --json | Write diagnostics as a JSON array. |
| --show-config-path | Write invalid configuration paths instead of diagnostic objects. |
| --experiment <name> | Enable one named experimental language feature. Repeatable. |
| --help, -h | Print help for the selected command. |
Discovery options
| Option | Meaning |
| --format <format> | text or json for find; text, tree, or dot for list. |
| --no-hidden | Exclude hidden directories during discovery. |
| --dependencies | Include dependency edges. Valid with DOT output. |
| --working-dir <path> | Set the discovery root. |
Global options
| Option | Meaning |
| --allow-path <dir> | Permit configurations to read this directory, which is otherwise refused for being outside the workspace root. Repeatable. |
| --non-interactive | Accepted and ignored; nothing here prompts. Also accepted as --terragrunt-non-interactive. |
| --no-color, --no-tips | Accepted and ignored. Also accepted under their --terragrunt- spellings. |
Terragrunt renamed those flags when it dropped the prefix, and scripts still pass the old names, so both spellings are accepted. Only flags this honours are aliased: a legacy flag with no equivalent here — --terragrunt-download-dir, say — is refused rather than accepted and quietly dropped, because a caller that believes a flag took effect is worse off than one told it did not.
The workspace boundary comes from this tool's other life as a language server, where a configuration is opened before anyone has vouched for it. Terragrunt has no such boundary, so a layout it accepts can be one this refuses — a deployment plan kept in a sibling checkout, for instance. Name each such directory rather than widening the root, which would have to reach an ancestor broad enough to stop being a boundary at all.
tghclp --allow-path /srv/plans init
Execution behavior
run and OpenTofu shortcut commands validate the selected configuration before starting the executable. Use --tf-path <path> to select a binary; the default is tofu, or the value of TG_TF_PATH / TERRAGRUNT_TFPATH.
| Option | Meaning |
| --all, -a | Run every discovered unit in dependency order, as terragrunt's run-all does. Without it, a directory holding more than one unit is an error rather than a guess. |
| --working-dir <path> | Directory holding the configuration to run. |
| --tf-path <path> | The OpenTofu or Terraform executable to invoke. |
tghclp run --working-dir ./infrastructure/app -- plan
tghclp plan --working-dir ./infrastructure/app
tghclp apply --all --working-dir ./infrastructure
tghclp run --tf-path terraform --working-dir ./app -- apply
The executable is spawned without a shell. Arguments are passed directly.
Generated files
A generate block writes its file with a signature line naming this tool. if_exists = "overwrite_terragrunt" then means what it says — overwrite a file this tool generated — and a file without that signature is refused rather than destroyed.
The signature is deliberately not terragrunt's own. Each tool refuses a file bearing the other's mark, so a directory generated into by one is not silently taken over by the other, and the message says which tool wrote what.
Reference
JavaScript API
Public exports from the tghclparser package root. The package provides ESM, CommonJS, and TypeScript declarations.
Core classes
new ParsedDocument(workspace, uri, content)
Parses one document immediately and exposes syntax, token, evaluation, and language-service queries.
| Method | Returns / effect |
| getUri() | The document URI. |
| getContent() | The current source text. |
| setContent(content) | Replaces source text and reparses the document. |
| getAST() | The parser AST, or null when unavailable. |
| getTokens() | Top-level Token[]. |
| getDiagnostics() | LSP Diagnostic[]. |
| getCompletionsAtPosition(position) | A promise of LSP CompletionItem[]. |
| getHoverInfo(position) | A promise of LSP markup, or null. |
| getLinks() | A promise of LSP DocumentLink[]. |
| findTokenAtPosition(position) | The narrowest token at an LSP position, or null. |
| getAllLocals() | A map of evaluated local values. |
| getAllOutputs() | A map of outputs discovered from Terraform state. |
| evaluateValue(token, targetName?) | The evaluated runtime value, when resolvable. |
new Workspace()
Owns documents and configuration relationships for a workspace root.
| Method | Returns / effect |
| setWorkspaceRoot(uri) | Sets the root as a URI string. |
| addDocument(document) | Adds or updates a parsed document and its relationships. |
| removeDocument(uri) | Removes an open document. |
| findTerragruntConfigs(rootDir) | Discovers configuration paths below a filesystem directory. |
| refreshDependencyTree() | Builds and returns TreeNode<TerragruntConfig>. |
| getParsedDocument(uri) | Loads or returns a parsed document. |
| getDependencies(uri) | Returns direct dependency configurations. |
| getDependents(uri) | Returns configurations that depend on the URI. |
| getReferencingConfigs(uri) | Returns all configurations referencing the URI. |
| getConfigTreeRoot() | Returns the most recently built graph root. |
| configureRemoteModules(policy, options) | Sets whether registry module sources (tfr://) are fetched, and how: credentials, the host approval callback, the cache directory. Until it is called, a unit naming a remote source has no module state. |
| onModuleVariablesChanged(listener) | Calls the listener with a unit's URI when a background fetch has landed and its diagnostics should be published again. |
| remoteModulesSettled() | Resolves once no fetch is in flight and every waiting unit has been updated. |
| clearRemoteModuleCache() | Deletes the fetched modules and fetches again for open units. |
new ConfigEvaluator(options)
Evaluates Terragrunt configuration values with explicit environment and trust settings.
const evaluator = new ConfigEvaluator({
environmentVariables: process.env,
workspaceTrusted: true,
terraformCommand: 'plan',
terraformCliArgs: []
});
| Method | Returns / effect |
| setWorkspaceTrusted(value) | Changes the evaluator trust state. |
| evaluateUnit(path, content, workDir) | { valid, inputs, error?, unresolved? }; evaluation errors are captured. unresolved is true when the error is a value that cannot be known yet, such as a declared dependency with no outputs, rather than a fault in the configuration. |
| evaluateRenderedConfig(path, content, workDir) | A complete RuntimeValue; errors are thrown. |
| evaluateAtPosition(path, content, workDir, position) | The value of the function call at the zero-based position, if any. |
| evaluatedSpans(path, content, workDir) | EvaluatedSpan[] — { start, end, kind, value } by character offset for every expression, and every attribute name (kind says which), whose value is not already spelled out in its source; literals written as their value are left out. One evaluation of the file. |
The resolveDependency(request) option reads a dependency's outputs. The evaluator finds the dependency block in the unit and the configurations merged into it, the way Terragrunt merges them, evaluates config_path and the mocks where they are written, and passes a DependencyRequest: { name, unitPath, declaredIn, targetConfigPath, targetDir, mockOutputs?, mockOutputsAllowedTerraformCommands? }, with config_path resolved against the unit's directory. The resolver resolves to { outputs: … }, or to undefined when the dependency has no outputs yet, which evaluation reports as unresolved. A dependency declared nowhere the unit merges is a fault the evaluator reports itself. Each dependency is resolved once per evaluation.
Schema.getInstance()
Returns the schema singleton used for file-kind, block, attribute, and function definitions.
Lookup methods include getFileKind, getRootBlockDefinitions, getRootAttributeDefinitions, getBlockDefinition, getAllBlockTemplates, getAllFunctions, and getFunctionDefinition. Snippet helpers and value-validation methods are also public.
FunctionRegistry.getInstance()
Returns the registry for named Terragrunt function implementations and serializable function operations.
Register implementations with registerFunction, operations with registerOperation, or a namespaced group with registerFunctionGroup. Duplicate names throw.
Utility exports
| Export | Purpose |
| runtimeValueToPlain | Converts nested typed runtime values and maps to plain JavaScript values. |
| Token | A source-positioned node with type, value, children, parent, decorators, and display helpers. |
| FunctionOperation | An ops-ts operation wrapper for a named function evaluator. |
| invokeFunctionOperation | Invokes an operation with serialized arguments and live evaluation context. |
| CompletionsProvider | Computes completion items from schema and document context. |
| DiagnosticsProvider | Computes schema and reference diagnostics. |
| HoverProvider | Computes markup content at a token. |
| LinkProvider | Resolves navigable links in a document. |
Primary types
RuntimeValue preserves an explicit value type. Objects and blocks contain Map<string, RuntimeValue>; arrays contain nested runtime values. TerragruntConfig records a configuration's URI, content, incoming and outgoing relationships, source and target paths, dependency type, and optional outputs.
Reference
Supported files
File names select a schema. The parser accepts general HCL syntax, while diagnostics and completions follow the Terragrunt file kind.
| File | Kind and role |
| terragrunt.hcl | unit — a deployable unit configuration. |
| root.hcl | unit — a conventional named shared configuration. |
| *.hcl | unit — named shared unit configurations use the unit schema. |
| terragrunt.stack.hcl | stack — explicit unit and stack component declarations. |
| terragrunt.values.hcl | values — values generated or consumed by stack components; arbitrary root attributes are allowed. |
| terragrunt.autoinclude.hcl | unit-autoinclude — configuration automatically applied to matching units. |
| terragrunt.autoinclude.stack.hcl | stack-autoinclude — configuration automatically applied to matching stacks. |
| terragrunt.hcl.json | JSON form of a unit configuration. |
| terragrunt.stack.hcl.json | JSON form of a stack configuration. |
| terragrunt.values.hcl.json | JSON values configuration; arbitrary object keys are accepted. |
Discovery
The CLI's find, list, and graph discovery commands look specifically for terragrunt.hcl and terragrunt.stack.hcl. Validation recognizes the broader supported filename set.
Discovery skips .git, .scrap, .terraform, .terragrunt-cache, .terragrunt-stack, .trash, and node_modules. With --no-hidden, it skips all hidden directories.
Language regime
Version 1 follows the current Terragrunt language regime. Removed or deprecated compatibility syntax is intentionally not accepted. The schema covers unit configuration, stack components, autoincludes, features, excludes, error policies, catalogs, IaC engines, CAS source controls, and Terragrunt functions.
Explanation
How the toolkit fits together
tghclparser separates syntax, language knowledge, workspace context, and semantic evaluation because each answers a different question.
Four layers, four questions
1 · GrammarWhat was written?
The Peggy grammar turns characters into an AST while retaining exact source locations.
2 · SchemaWhat is allowed here?
File-kind-aware definitions describe valid blocks, attributes, functions, parameters, and value types.
3 · WorkspaceWhat does this connect to?
Documents become a graph of includes, dependencies, reads, stacks, units, and state outputs.
4 · EvaluatorWhat does it mean?
Typed runtime values resolve locals, references, expressions, includes, and supported functions.
Why tokens sit between syntax and features
The raw parser AST closely follows grammar rules. A ParsedDocument also builds a navigable Token tree. Tokens normalize the shapes that completions, hovers, links, diagnostics, and reference resolution need while preserving parent/child relationships and source ranges.
This keeps editor providers from each having to reinterpret raw parser nodes. It also gives callers a stable vocabulary such as block, attribute, reference, function_call, and interpolation.
Why a document needs a workspace
Terragrunt configuration is rarely meaningful in isolation. An include can expose locals, a dependency can expose state outputs, a stack can declare generated component targets, and a function can read another configuration. The workspace owns that cross-file context.
A document can still report local syntax and schema diagnostics immediately. Registering it with a workspace adds the context needed for reference-aware results and graph queries.
Why runtime values keep their types
Evaluation does not collapse everything into plain JavaScript. RuntimeValue<T> carries an explicit Terragrunt value type, and nested collections contain more runtime values. This allows providers to retain type knowledge, sensitive wrappers, and origin information while computing results. Convert to plain values only at an application boundary with runtimeValueToPlain.
Language service versus execution
Parsing and most editor assistance are observational: they analyze text. Semantic evaluation can read workspace files and invoke selected functions. Command execution goes further by starting an OpenTofu, Terraform, or explicitly requested external program. Those capabilities are kept behind separate APIs and trust checks.
Explanation
Trust and evaluation
Parsing text is different from evaluating a repository. The trust boundary makes that distinction explicit.
Parsing is local analysis
Constructing a ParsedDocument parses its supplied string, creates tokens, and computes immediate diagnostics. It does not need permission to execute arbitrary programs.
Evaluation can observe the workspace
Terragrunt semantics include functions that read files, find parent configurations, inspect environment variables, and derive paths. Accurate evaluation therefore needs a filesystem context. ConfigEvaluator refuses semantic evaluation unless workspaceTrusted is explicitly true.
Trust is a caller decisionSet workspaceTrusted: true only after your application has established that the user trusts the workspace. Do not infer trust from a file extension or from successful parsing.
Paths stay inside the workspace root
Trusted file access resolves real paths and rejects targets outside the workspace root. Resolving real paths is important because a path that looks internal can escape through a symbolic link.
Function operations separate data from capabilities
Function evaluation uses named operations. Arguments travel through a serializable dry context; live services such as filesystem access or command execution travel through a wet context. This boundary makes capability-bearing context visible instead of smuggling it into ordinary values.
Remote modules reach the network
A unit whose terraform { source } names a registry module can have that module fetched, so its variables drive input completion, hover and checks. This is the one place the language service contacts a network, and it does so only when the caller asks: Workspace.configureRemoteModules takes a policy that must be both enabled and trusted, and an approveHost callback that is asked before the first contact with each registry or git host. A workspace that never calls it leaves remote sources alone.
A registry token is sent to its registry host alone, over TLS. Every host a request reaches, including one a registry redirects to, must be named rather than addressed and must resolve outside the loopback and link-local ranges. Git runs without a shell, with prompts disabled and the repository URL kept off its command line. Only the top-level .tf files of a module are kept, read from the object store or the archive stream without unpacking anything, into an owner-only per-user cache.
The CLI does not invoke a shell
run and exec spawn the selected executable with an argument array and shell: false. Shell operators in an argument are not interpreted as shell syntax. This removes a common command-injection surface, although callers must still choose trusted executables and arguments.
Recommended integration policy
- Allow parsing and local diagnostics before workspace trust.
- Gate semantic evaluation and workspace file reads on explicit trust.
- Keep the workspace root as narrow as practical.
- Pass environment variables intentionally; they are part of evaluation input.
- Require a separate user action before executing infrastructure commands.
- Fetch remote modules only in a trusted workspace, and ask the user before the first contact with each host.