loopx is a CLI tool that automates repeated execution ("loops") of scripts, primarily designed to wrap agent CLIs. It provides a scriptable loop engine with structured output, control flow between scripts, environment variable management, and a script installation mechanism.
Package name: loopx
Implementation language: TypeScript
Module format: ESM-only
Target runtimes: Node.js ≥ 20.6, Bun ≥ 1.0
Platform support: POSIX-only (macOS, Linux) for v1. Windows is not supported.
Note: The Node.js minimum was raised from 18 to 20.6 to support
module.register(), which is required for the custom module loader used to resolveimport from "loopx"in scripts (see section 3.3).
A script is an executable unit located in the .loopx/ directory relative to the current working directory. Scripts come in two forms:
A single file with a supported extension:
- Bash (
.sh) - JavaScript (
.js/.jsx) - TypeScript (
.ts/.tsx)
.mjs and .cjs extensions are intentionally unsupported. All JS/TS scripts must be ESM (see section 6.3).
A file script is identified by its base name (filename without extension). For example, .loopx/myscript.ts is identified as myscript.
A directory containing a package.json with a main field pointing to a file with a supported extension. The script name is the directory name.
.loopx/
my-pipeline/
package.json ← { "main": "index.ts", ... }
index.ts
node_modules/
...
Directory scripts allow scripts to have their own dependencies managed via standard npm install or bun install within the directory. loopx does not auto-install dependencies. If node_modules/ is missing and the script fails to import a package, the resulting error is a normal Node.js module resolution error.
A directory in .loopx/ is only recognized as a script if it contains a package.json with a main field. Directories without this are ignored (and may exist for other purposes such as shared utilities).
Important: Directory scripts must not list loopx as their own dependency. The loopx helpers (output, input) are provided automatically by the running CLI via a custom module loader (see section 3.3). Installing a separate version of loopx inside a directory script may cause version mismatches.
A loop is a repeated execution cycle modeled as a state machine. Each iteration runs a target script, examines its structured output, and transitions:
gotoanother script: transition to that script for the next iteration.- No
goto: the cycle ends and the loop restarts from the starting target. stop: the machine halts.
The starting target is the original script specified when loopx was invoked — either a named script or the default script. The goto mechanism is a state transition, not a permanent reassignment. When a target finishes without its own goto, execution returns to the starting target. The loop always resets to its initial state after a transition chain completes.
Self-referencing goto: A script may goto itself (e.g., script A outputs { goto: "A" }). This is a normal transition and counts as an iteration.
Example:
Starting target: A (script)
Iteration 1: A runs → outputs goto:"B"
Iteration 2: B runs → outputs goto:"C"
Iteration 3: C runs → outputs (no goto)
Iteration 4: A runs → (back to starting target)
Every iteration produces an output conforming to:
interface Output {
result?: string;
goto?: string;
stop?: boolean;
}Stdout is reserved for the structured output payload. Any human-readable logs, progress messages, or debug output from scripts must go to stderr.
Parsing rules:
- Only a top-level JSON object can be treated as structured output. Arrays, primitives (strings, numbers, booleans), and
nullfall back to raw result treatment. - If stdout is a valid JSON object containing at least one known field (
result,goto,stop), it is parsed as structured output. - If stdout is not valid JSON, is not an object, or is a valid JSON object but contains none of the known fields, the entire stdout content is treated as
{ result: <raw output> }. - Empty stdout (0 bytes) is treated as
{ result: "" }. This is the default case for scripts that produce no output, and causes the loop to reset (nogoto, nostop). - Extra JSON fields beyond
result,goto, andstopare silently ignored. - If
resultis present but not a string, it is coerced viaString(value). This includesnull:{"result": null}produces result"null". - If
gotois present but not a string, it is treated as absent. stopmust be exactlytrue(boolean). Any other value (including truthy strings like"true", numbers, etc.) is treated as absent. This prevents surprises like{"stop": "false"}halting the loop.
Field precedence:
stop: truetakes priority overgoto. If both are set, the loop halts.gotowith noresultis valid: the target script receives empty stdin.resultis only piped to the next script whengotois present. When the loop resets to the starting target (nogoto), the starting target receives empty stdin regardless of whether the previous iteration produced aresult.
loopx is installed globally to provide the loopx CLI command:
npm install -g loopx
A global install is sufficient for all loopx functionality, including JS/TS scripts that import { output, input } from "loopx". loopx uses a custom module loader to make its exports available to scripts regardless of install location (see section 3.3).
A project may pin a specific loopx version by installing it as a local dependency:
npm install --save-dev loopx
A local install provides two guarantees:
-
CLI delegation: When the globally installed
loopxbinary starts, it checks whether the current working directory (or an ancestor) has a localnode_modules/.bin/loopx. If found, the global instance delegates execution to the local version's binary before any command handling. This ensures the entire session — CLI behavior, script helpers, and all — uses the pinned version. -
Importable library: Application code can
import { run, runPromise } from "loopx"when loopx is a local dependency. This is standard Node.js module resolution — no special mechanism required.
Delegation rules:
- Nearest ancestor wins. loopx searches from the current working directory upward and delegates to the first
node_modules/.bin/loopxfound. - Recursion guard. The delegated process is spawned with
LOOPX_DELEGATED=1in its environment. If this variable is set when loopx starts, delegation is skipped. This prevents infinite delegation loops. - After delegation,
LOOPX_BINcontains the resolved realpath of the effective binary (the local version), not the original global launcher or any intermediate symlinks.
Scripts spawned by loopx (in .loopx/) need access to the output and input helpers via import { output, input } from "loopx".
For Node.js / tsx: loopx uses Node's --import flag to preload a registration module that installs a custom module resolve hook via module.register(). This hook intercepts bare specifier imports of "loopx" and resolves them to the running CLI's package exports. This approach works correctly with Node's ESM resolver, which does not support NODE_PATH.
For Bun: Bun's module resolver supports NODE_PATH for both CJS and ESM. loopx sets NODE_PATH to include its own package directory when running under Bun.
In both cases, the resolution always points to the post-delegation version. If a local install triggered delegation, the helpers resolve to the local version's package. This ensures script helpers match the running CLI version.
loopx injects a LOOPX_BIN environment variable into every script's execution environment. This variable contains the resolved realpath of the effective loopx binary (post-delegation), allowing bash scripts to call loopx subcommands reliably:
#!/bin/bash
$LOOPX_BIN output --result "done" --goto "next-step"loopx [options] [script-name]
- If
script-nameis provided, loopx looks for a script with that base name in.loopx/. - If
script-nameis omitted, loopx looks for a script nameddefaultin.loopx/. - If no
defaultscript exists and no script name is given, loopx exits with an error message instructing the user to create a script (e.g., "No default script found. Create.loopx/default.tsor specify a script name."). - If
script-namedoes not match any script in.loopx/, loopx exits with an error. loopx default(explicitly naming the default script) is valid and runs the script nameddefault, identical toloopxwith no script name.
| Flag | Description |
|---|---|
-n <count> |
Maximum number of loop iterations (see section 7.1 for counting semantics). Must be a non-negative integer; negative values or non-integers are usage errors. -n 0 validates the starting target (script discovery, name resolution, env file loading) but executes zero iterations, then exits with code 0. |
-e <path> |
Path to a local env file (.env format). The file must exist; a missing file is an error. Variables are merged with global env vars; local values take precedence on conflict. |
-h, --help |
Print usage information. Dynamically lists available scripts discovered in .loopx/. Performs non-fatal discovery and validation — if .loopx/ is missing or contains invalid scripts, help is still displayed with warnings appended. |
Flag precedence: A top-level -h / --help takes precedence over other top-level arguments and flags, and exits 0 without running scripts or subcommands.
Duplicate flags: Repeating -n or -e (e.g., -n 5 -n 10 or -e .env1 -e .env2) is a usage error. loopx exits with code 1.
Prints the installed version of loopx and exits.
A helper for bash scripts to emit structured output:
loopx output [--result <value>] [--goto <script-name>] [--stop]Prints the corresponding JSON to stdout. At least one flag must be provided; calling loopx output with no flags is an error.
Example usage in a bash script:
#!/bin/bash
# do work...
$LOOPX_BIN output --result "done" --goto "next-step"
exit 0Sets a global environment variable stored in the loopx global config directory.
Validation: The variable name must match [A-Za-z_][A-Za-z0-9_]*. Values containing \n or \r are rejected, since multiline values are not supported by the env file format.
Serialization: loopx env set writes the value as KEY="<literal value>" followed by a newline. No escape sequences are applied — the value is written literally within double quotes. This ensures reliable round-tripping for values containing spaces, #, =, quotes, and trailing spaces.
Removes a global environment variable. If the variable does not exist, this is a silent no-op (exits with code 0).
Lists all currently set global environment variables. Output format is one KEY=VALUE pair per line, sorted lexicographically by key name. If no variables are set, produces no output.
Installs a script into the .loopx/ directory. See section 10 for full details. Supports:
org/reposhorthand — expands tohttps://github.com/org/repoand clones as a directory script.- Git URL — clones a repository as a directory script.
- Tarball URL — extracts an archive as a directory script.
- Single-file URL — downloads a single script file.
Creates the .loopx/ directory if it does not exist.
Scripts are discovered by scanning the .loopx/ directory in the current working directory. The .loopx/ directory is only searched in the current working directory — ancestor directories are not searched.
- File scripts: Top-level files with supported extensions (
.sh,.js,.jsx,.ts,.tsx). The script name is the base name (filename without extension). - Directory scripts: Top-level directories containing a
package.jsonwith amainfield pointing to a file with a supported extension. The script name is the directory name. Themainfield must point to a file within the script's own directory — paths containing../or otherwise escaping the directory are rejected. A directory is ignored and a warning is printed to stderr if any of the following are true:package.jsonis unreadable or invalid JSON;mainis missing or not a string;mainpoints to a file without a supported extension;mainescapes the directory; ormainpoints to a file that does not exist.
Nested directories that do not contain a valid package.json with main are ignored.
Symlink policy: Symlinks within .loopx/ are followed during discovery. A symlinked file or directory is treated identically to its target. However, the main field in a directory script's package.json must still resolve to a path within the script's directory after symlink resolution — it must not escape the directory boundary.
Discovery metadata is cached at loop start for the duration of the loop. This means:
- Scripts added, removed, or renamed during loop execution are not detected until the next invocation.
- Changes to a
package.jsonmainfield are not detected until the next invocation. - Edits to the contents of an already-discovered script file take effect on subsequent iterations, because the child process reads the file from disk each time it is spawned.
Warnings (invalid main field, unsupported extensions in directories, paths escaping the script directory) are printed to stderr during discovery. Discovery runs at loop start for script mode and during --help.
If multiple entries share the same script name — whether file-to-file (e.g., example.sh and example.js), or file-to-directory (e.g., example.ts and example/) — loopx refuses to start and displays an error message listing the conflicting entries.
The following script names are reserved and cannot be used:
outputenvinstallversion
If any script in .loopx/ uses a reserved name, loopx refuses to start and displays an error message.
- Script names must not begin with
-. - Script names must match the pattern
[a-zA-Z0-9_][a-zA-Z0-9_-]*(start with alphanumeric or underscore, followed by alphanumerics, underscores, or hyphens).
If any script in .loopx/ violates these restrictions, the behavior depends on the command: in run mode, loopx refuses to start and displays an error message. In help mode, the invalid script is listed with a non-fatal warning.
Not all commands require .loopx/ to exist or be valid:
| Command | Requires .loopx/ |
Validates scripts |
|---|---|---|
loopx version |
No | No |
loopx env * |
No | No |
loopx output |
No | No |
loopx -h / --help |
No | Non-fatal (warnings shown) |
loopx install <url> |
No (creates if needed) | No |
loopx [script-name] |
Yes | Yes |
The working directory for script execution depends on the script type:
- File scripts: Run with the directory where
loopxwas invoked as the working directory. - Directory scripts: Run with the script's own directory as the working directory (e.g.,
.loopx/my-pipeline/), so relative imports andnode_modules/resolve naturally.
loopx injects LOOPX_PROJECT_ROOT into every script's environment, set to the absolute path of the directory where loopx was invoked. This is essential for directory scripts that need to reference project files outside their own directory.
Bash scripts (.sh) are executed as child processes via /bin/bash. The script's stdout is captured as its structured output. Stderr is passed through to the user's terminal.
JavaScript and TypeScript scripts are executed as child processes using tsx, which handles .js, .jsx, .ts, and .tsx files uniformly. tsx is a dependency of loopx and does not need to be installed separately by the user.
JS/TS scripts must be ESM and must use import, not require. CommonJS is not supported. .mjs and .cjs extensions are intentionally unsupported.
- Stdout is captured as structured output.
- Stderr is passed through to the user's terminal.
When running under Bun, loopx uses Bun's native TypeScript/JSX support instead of tsx.
For directory scripts, loopx reads the main field from the directory's package.json to determine the entry point file. The entry point is then executed using the same rules as file scripts — bash for .sh, tsx/bun for JS/TS extensions.
When imported from loopx, the output() function writes structured JSON to stdout and terminates the process.
import { output } from "loopx";
output({ result: "hello", goto: "next-step" });
// process exits here — no code after this line runsBehavior:
output()flushes stdout before callingprocess.exit(0), ensuring the JSON payload is not lost.- Since
output()callsprocess.exit(), calling it multiple times is not possible — only the first call takes effect. - The argument must be an object containing at least one known field (
result,goto, orstop) with a defined value. Callingoutput({})(no known fields) throws an error. - Properties whose value is
undefinedare treated as absent (they are omitted during JSON serialization). For example,output({ result: "done", goto: undefined })is equivalent tooutput({ result: "done" }). - If called with a non-object value (e.g., a plain string, number, or boolean), the value is serialized as
{ result: String(value) }. Arrays are not treated as non-object values (sincetypeof [] === 'object'); an array must contain at least one known field with a defined value, just like any other object — sooutput([1,2,3])throws an error (no known fields). - If called with
nullorundefined, an error is thrown.
When imported from loopx, the input() function reads the input piped from the previous script via stdin:
import { input, output } from "loopx";
const data = await input(); // Returns the input string, or empty string if no input
output({ result: `processed: ${data}` });input() returns a Promise<string>. On the first iteration (when no input is available), it resolves to an empty string.
The result is cached: calling input() multiple times within the same script execution returns the same string each time.
When a script's output includes both result and goto, the result value is delivered to the next script via stdin — the result string is written to the next script's stdin.
result is only piped when goto is present. When the loop resets to the starting target (no goto in the output), the starting target receives empty stdin, regardless of any result value in the previous output.
The first script invocation in a loop receives no input. Stdin is empty.
- Validate the
.loopx/directory (check for name collisions, reserved names, name restrictions). Cache the discovery results. - Load environment variables (global + local via
-e). Cache the resolved set for the duration of the loop. - Determine the starting target: named script or
defaultscript. - If
-n 0was specified: exit with code 0 (no iterations executed). - Execute the starting target with no input (first iteration).
- Capture stdout. Parse it as structured output per section 2.3.
- Increment the iteration counter.
- If
stopistrue: exit with code 0. - If
-nwas specified and the iteration count has been reached: exit with code 0. The output from this final iteration is still yielded/observed before termination. - If
gotois present: a. Validate that the named script exists in the cached discovery results. If not found, print an error and exit with code 1. b. Execute thegotoscript withresultpiped via stdin (or empty stdin ifresultis absent). c. Return to step 6 with the new script's output. - If
gotois absent: a. Re-run the starting target with no input. b. Return to step 6.
Iteration counting: -n / maxIterations counts every target execution, including goto hops — not just returns to the starting target. For example, if script A outputs goto: "B" and B outputs goto: "C", that is three iterations (A, B, C).
The CLI does not print result to its own stdout at any point. All human-readable output from scripts should go to stderr, which passes through to the terminal. Structured results are accessed via the programmatic API (section 9).
- Non-zero exit code from a script: The loop stops immediately. loopx exits with code 1. The script's stderr has already been passed through to the terminal. Any stdout produced by the script before it failed is not parsed as structured output.
- Invalid
gototarget: Ifgotoreferences a script name that does not exist in.loopx/, loopx prints an error message to stderr and exits with code 1. - Missing
.loopx/directory: When running a named or default script, if.loopx/does not exist, loopx exits with an error instructing the user to create it.
loopx handles process signals to ensure clean shutdown:
- SIGINT / SIGTERM: The signal is forwarded to the active child process group (not just the direct child). This ensures grandchild processes (e.g., agent CLIs spawned by scripts) also receive the signal, preventing orphaned processes.
- Grace period: After forwarding the signal, loopx waits 5 seconds for the child process group to exit. If the process group has not exited after 5 seconds, loopx sends SIGKILL to the process group.
- Exit code: After the child exits, loopx exits with code
128 + signal number(standard POSIX convention, e.g., 130 for SIGINT). - Between iterations: If no child process is running (e.g., between iterations), loopx exits immediately with the appropriate signal exit code.
Global environment variables are stored in the loopx configuration directory at:
$XDG_CONFIG_HOME/loopx/env
If XDG_CONFIG_HOME is not set, it defaults to ~/.config, resulting in ~/.config/loopx/env.
The file uses .env format with the following rules:
- One
KEY=VALUEpair per line. - No whitespace is permitted around
=. The key extends to the first=, and the value is everything after it to the end of the line (trimmed of trailing whitespace). - Lines starting with
#are comments. Inline comments are not supported — a#after a value is part of the value. - Blank lines are ignored.
- Duplicate keys: last occurrence wins.
- Values are single-line strings. Values may be optionally wrapped in double quotes (
") or single quotes ('), which are stripped. No escape sequence interpretation — content inside quotes is treated literally (e.g.,"\n"is a backslash followed byn, not a newline). - No multiline value support.
- Key validation: Only keys matching
[A-Za-z_][A-Za-z0-9_]*are recognized from env files (both global and local). Non-blank, non-comment lines that do not contain a valid key (e.g., lines without=, lines with invalid key names like1BAD=valorKEY WITH SPACES=val) are ignored with a warning to stderr.
If the directory or file does not exist, loopx treats it as having no global variables. The directory is created on first loopx env set.
Concurrent mutation: Concurrent writes to the same global env file (e.g., multiple simultaneous loopx env set calls) are not guaranteed to be atomic in v1. The result is undefined.
Environment variables are loaded once at loop start and cached for the duration of the loop. Changes to env files during loop execution are not picked up until the next invocation.
When -e <path> is specified, the file at <path> is read using the same .env format rules. If the file does not exist, loopx exits with an error.
Local variables are merged with global env vars. Local values take precedence on conflict.
All resolved environment variables are injected into the script's execution environment alongside the inherited system environment, with the following precedence (highest wins):
- loopx-injected variables (
LOOPX_BIN,LOOPX_PROJECT_ROOT) — always override any user-supplied values of the same name. - Local env file (
-e) values. - Global loopx env (
$XDG_CONFIG_HOME/loopx/env) values. - Inherited system environment.
loopx injects the following variables into every script execution:
| Variable | Value |
|---|---|
LOOPX_BIN |
Resolved realpath of the effective loopx binary (post-delegation) |
LOOPX_PROJECT_ROOT |
Absolute path to the directory where loopx was invoked |
Note: For Node.js/tsx, module resolution for import from "loopx" is handled via --import and a custom resolve hook (see section 3.3), not via NODE_PATH. For Bun, NODE_PATH is set internally but is not considered a user-facing injected variable.
loopx can be imported and used from TypeScript/JavaScript. This requires loopx to be installed as a local dependency (npm install loopx or npm install --save-dev loopx).
import { run } from "loopx";
const loop = run("myscript");
for await (const output of loop) {
console.log(output.result);
// each yielded value is an Output from one iteration
}
// loop has ended (stop: true or max iterations reached)Returns an AsyncGenerator<Output> that yields the Output from each loop iteration. The generator completes when the loop ends via stop: true or when maxIterations is reached. The output from the final iteration is always yielded before the generator completes.
Options can be passed as a second argument:
import { run } from "loopx";
for await (const output of run("myscript", { maxIterations: 10, envFile: ".env" })) {
// ...
}Early termination: If the consumer breaks out of the for await loop or calls generator.return(), loopx terminates the active child process group (SIGTERM, then SIGKILL after 5 seconds) and cleans up.
import { runPromise } from "loopx";
const outputs: Output[] = await runPromise("myscript");Returns a Promise<Output[]> that resolves with an array of all Output values when the loop ends. Accepts the same options object as run().
The programmatic API has different behavior from the CLI:
- The library never prints
resultto stdout. All results are returned as structuredOutputobjects. - Errors throw/reject. Any condition that would cause the CLI to exit with code 1 (non-zero script exit, invalid
goto, validation failures) causesrun()to throw from the generator andrunPromise()to reject. - Partial outputs are preserved. When
run()throws, all previously yielded outputs have already been consumed by the caller. WhenrunPromise()rejects, partial outputs are not available (userun()if partial results matter). - Stderr passes through. Script stderr is still forwarded to the parent process's stderr, same as in CLI mode.
These functions are documented in sections 6.5 and 6.6. They are designed for use inside scripts, not in application code that calls run() / runPromise().
import type { Output, RunOptions } from "loopx";
interface Output {
result?: string;
goto?: string;
stop?: boolean;
}
interface RunOptions {
maxIterations?: number;
envFile?: string;
signal?: AbortSignal;
cwd?: string;
}- When
signalis provided and aborted, the active child process group is terminated and the generator/promise completes with an abort error. cwdspecifies the working directory for script resolution and execution. Defaults toprocess.cwd()at the timerun()orrunPromise()is called. The.loopx/directory is resolved relative to this path.maxIterationscounts every target execution, including goto hops.maxIterations: 0mirrors CLI-n 0behavior: validates and exits without executing any iterations.maxIterationsmust be a non-negative integer; invalid values (negative, non-integer, NaN) causerun()to throw andrunPromise()to reject before execution begins.- Relative
envFilepaths are resolved againstcwdif provided, otherwise againstprocess.cwd()at call time.
loopx install <source>
Installs a script into the .loopx/ directory, creating it if necessary.
Sources are classified using the following rules, applied in order:
org/reposhorthand: A source matching the pattern<org>/<repo>(no protocol prefix, exactly one slash, no additional path segments) is expanded tohttps://github.com/<org>/<repo>.gitand treated as a git source.- Known git hosts: A URL whose hostname is
github.com,gitlab.com, orbitbucket.orgis treated as a git source only when the pathname is exactly/<owner>/<repo>or/<owner>/<repo>.git, optionally with a trailing slash. Other URLs on these hosts (e.g., tarball download URLs, raw file URLs, paths with additional segments like/org/repo/tree/main) continue through the remaining source-detection rules. .gitURL: Any other URL ending in.gitis treated as a git source.- Tarball URL: A URL ending in
.tar.gzor.tgzis downloaded and extracted as a directory script. - Single-file URL: Any other URL is treated as a single file download.
loopx install myorg/my-agent-script
# equivalent to: loopx install https://github.com/myorg/my-agent-script.git
loopx install https://github.com/myorg/my-agent-script
# also treated as git (github.com host detected)
- The filename is derived from the URL's last path segment, with query strings and fragments stripped.
- The file must have a supported extension (
.sh,.js,.jsx,.ts,.tsx); otherwise an error is displayed. - The script name is the base name of the downloaded file.
- The repository is cloned with
--depth 1(shallow clone) into.loopx/<repo-name>/. - The script name is derived from the repository name (last path segment, minus
.gitsuffix if present). - The cloned directory must contain a
package.jsonwith amainfield pointing to a supported extension. If not, the clone is removed and an error is displayed.
- The archive is downloaded and extracted.
- If extraction yields a single top-level directory, that directory is treated as the package root and moved to
.loopx/<archive-name>/. If extraction yields multiple top-level entries, the extracted contents are placed directly in.loopx/<archive-name>/. archive-nameis the URL's last path segment minus archive extensions (.tar.gz,.tgz), with query strings and fragments stripped (same as single-file URLs).- The resulting directory must contain a
package.jsonwith amainfield pointing to a supported extension. If not, the directory is removed and an error is displayed.
All install sources share these rules:
- If a script with the same name (regardless of whether it's a file or directory script) already exists in
.loopx/, loopx displays an error and does not overwrite. The user must manually remove the existing script first. - The script name is validated against reserved name and name restriction rules before being saved.
- loopx does not run
npm installorbun installafter cloning/extracting. For directory scripts with dependencies, the user must install them manually (e.g.,cd .loopx/my-script && npm install). - Install failure cleanup: Any install failure (download error, HTTP non-2xx, git clone failure, extraction failure, post-download validation failure) exits with code 1. Any partially created target file or directory at the destination path is removed before exit.
loopx -h / loopx --help prints usage information including:
- General usage syntax
- Available options and subcommands
- A dynamically generated list of scripts discovered in the current
.loopx/directory (name and file type)
Help performs non-fatal discovery and validation: if .loopx/ does not exist, help is displayed without the script list. If .loopx/ exists but contains validation errors (name collisions, reserved names), help is displayed with warnings about the invalid scripts.
| Code | Meaning |
|---|---|
| 0 | Clean exit: loop ended via stop: true, -n limit reached (including -n 0), or successful subcommand execution. |
| 1 | Error: script exited non-zero, validation failure, invalid goto target, missing script, missing .loopx/ directory, or usage error (invalid -n value, missing -e file). |
| 128+N | Interrupted by signal N (e.g., 130 for SIGINT). |
Note: A non-zero exit code from any script causes loopx to exit with code 1. Scripts that need error resilience should handle errors internally and exit 0.
| Name | Context | Purpose |
|---|---|---|
output |
Script name | Reserved for loopx output subcommand |
env |
Script name | Reserved for loopx env subcommand |
install |
Script name | Reserved for loopx install subcommand |
version |
Script name | Reserved for loopx version subcommand |
default |
Script name | The script run when no name is provided |
LOOPX_BIN |
Env variable | Resolved realpath of the effective loopx binary (post-delegation) |
LOOPX_PROJECT_ROOT |
Env variable | Absolute path to the directory where loopx was invoked |
LOOPX_DELEGATED |
Env variable | Set to 1 during delegation to prevent recursion |