Configuration

This page is the authoritative inventory of Jaiph configuration keys: every key, its value type, default, environment-variable equivalent, and precedence. For environment-variable details (defaults, scopes) see Environment variables. For the CLI flags that front-end the same knobs see CLI.

Configuration sources, in priority order:

  1. Environment variables — locked once observed by the host CLI; see Locked variables.
  2. Def-level config { … } — applies for the duration of that def.
  3. Module-level config { … } — applies to all defs in that file unless overridden.
  4. Built-in defaults — lowest priority.

Config block syntax

Aspect Rule
Module-level At most one config { … } block per .jh file. May appear anywhere among top-level constructs.
Def-level At most one nested config { … } per def body. Must be the first non-comment construct in the body.
Allowed module-level keys agent.*, run.*, and module.*.
Allowed def-level keys agent.* and run.*. A module.* key in a def-level block is E_PARSE (module.* keys are not allowed in def-level config (only agent.* and run.* keys)). Any other unrecognized key is the unknown-key error below — including the removed trusted_envs.
Duplicate block E_PARSE duplicate config block (only one allowed per file) / E_PARSE duplicate config block inside def (only one allowed per def).
Unknown key E_PARSE unknown config key: <key>. Allowed: … (lists every allowed key).
Wrong value type E_PARSE.

Value syntax

Global rule: Every Jaiph string position accepts three equivalent forms:

Form Example Stored as
Bare identifier model ${model}
Double-quoted string "${model}" or "prefix-${model}" string content as-is
Bare interpolation ref ${model} or ${model.field} ${model} / ${model.field}

All three resolve identically at runtime. The only interpolation Jaiph understands is ${name} / ${name.field} — shell expansion forms (${var:-default}, ${var//…}, ${#var}) are not. Written as a bare config value they fail with E_PARSE (config value must be a quoted string, bare identifier, or true/false); written inside a double-quoted config value they are stored as literal text and never expanded.

Type Format Example
String Double-quoted; supports \\, \n, \t, \", and ${name} interpolation "gpt-4", "${model}"
String (bare identifier sugar) Bare identifier — stored as ${name} and resolved at runtime model
String (bare ref sugar) Bare ${name} or ${name.field} — stored as-is and resolved at runtime ${model}, ${model.field}
Boolean Bare true / false true
Integer Unsigned decimal digits 300

String config values support the same ${identifier} interpolation as orchestration strings (log, prompt, return, etc.). A bare identifier or bare ${name} on the RHS is sugar for a single ${identifier} reference (for example agent.model = model, agent.model = ${model}, and agent.model = "${model}" are all equivalent).

Interpolation scope:

Config level Available identifiers
Module-level Module const values and environment variables
Def-level Module const values, environment variables, and that def’s parameters

Interpolation runs when the config scope is applied (def entry for def-level keys; CLI startup for module-level keys). Environment variables still win over in-file config when locked.

Agent keys

Key Type Default Env equivalent Notes
agent.model string — JAIPH_AGENT_MODEL (env only) Model for prompt steps in this scope. Resolved at each prompt invocation and passed as a per-call --model flag — it does not set JAIPH_AGENT_MODEL in the run environment, so scripts and other steps do not see it. Set JAIPH_AGENT_MODEL in the shell to override all prompts in a run.
agent.command string cursor-agent JAIPH_AGENT_COMMAND Cursor backend command. Basename other than cursor-agent enables custom-command mode (stdin → command → stdout). Entry module only — imported modules cannot set this key by default (see Import trust boundary).
agent.backend string (cursor | claude | codex) cursor JAIPH_AGENT_BACKEND Backend selector. Entry module only — imported modules cannot set this key by default (see Import trust boundary).
agent.trusted_workspace string (path) workspace root JAIPH_AGENT_TRUSTED_WORKSPACE Directory passed to Cursor as --trust. When unset, defaults to JAIPH_WORKSPACE. A relative path in the entry module’s module-level config is resolved against the workspace root to an absolute path at CLI startup. Values applied at runtime — a def-level block or an imported module — are assigned to the env var exactly as authored (not normalized).
agent.cursor_flags string — JAIPH_AGENT_CURSOR_FLAGS Extra flags appended to Cursor invocations (whitespace-split).
agent.claude_flags string — JAIPH_AGENT_CLAUDE_FLAGS Extra flags appended to Claude invocations (whitespace-split).

Run keys

Key Type Default Env equivalent Notes
run.logs_dir string (path) .jaiph/runs JAIPH_RUNS_DIR Step log directory. Relative paths join the workspace root; absolute paths are used as-is.
run.debug boolean false JAIPH_DEBUG Enable debug tracing.
run.recover_limit integer 10 — (no env override) Maximum attempts for run … recover loops before the step fails. Resolves via def > module > default.

Module keys

Informational metadata only; does not affect execution. Allowed in module-level config only — any module.* key inside a def-level config is E_PARSE.

Key Type Default
module.name string —
module.version string —
module.description string —

Script env keys (use + --env)

The trusted_envs config key is removed (E_PARSE unknown config key). Scripts are sterile by default: a script subprocess receives only process mechanics, the JAIPH_* script contract keys, and the host keys its own declaration requests with a use clause — and a use key crosses only when the operator granted it with --env KEY[=VALUE]:

script release use GITHUB_TOKEN NPM_TOKEN = 'gh release create …'
jaiph run --env GITHUB_TOKEN --env NPM_TOKEN publish.jh

The use request lives on a script declaration (named script, export script, or import script … as alias use KEY) or on a named prompt definition (prompt analyze(log) use GITHUB_TOKEN = …) — never on defs, call or prompt sites, or anonymous prompt steps. Anonymous prompt subprocesses keep the fail-closed scrubPromptEnv allowlist and never receive --env secrets in their child env; a named prompt’s granted use keys are injected into its agent subprocess on top of that scrub. That is spawn-env, not a sandbox — see Why Jaiph. Recipe: Pass a host key to a script. Also Language — Subprocess environment and Environment variables.

Precedence

Agent and run keys

Layer Effect
Environment (JAIPH_AGENT_*, JAIPH_RUNS_DIR, JAIPH_DEBUG) Locked when present in the parent env; cannot be overridden by module- or def-level config.
Def-level config Applies for the def body; restored on exit.
Module-level config Applies to defs without their own block.
Built-in defaults Lowest priority.

Scoping across nested calls

Call type Scope behaviour
Root entry (jaiph run file.jh) Full module + def metadata applied with normal precedence.
Same-module call Callee’s def-level config is layered on top of the caller’s effective env. Module-level config is not re-applied.
Cross-module call (e.g. alias.main()) Callee’s module-level config is layered, then def-level on top — same as root-entry precedence, respecting ${NAME}_LOCKED. The entry-module-only keys (agent.command, agent.backend, agent.trusted_workspace, agent.cursor_flags, agent.claude_flags, run.logs_dir) are not applied from imported modules (see Import trust boundary).

After any nested call returns, the caller’s scope is restored exactly as before.

Locked variables

When the host CLI builds the runner environment, any of these variables already present in process.env gets a matching ${NAME}_LOCKED=1 flag set on the child env. The runtime refuses to overwrite a locked value from later metadata merges.

Locked names: JAIPH_AGENT_BACKEND, JAIPH_AGENT_MODEL, JAIPH_AGENT_COMMAND, JAIPH_AGENT_TRUSTED_WORKSPACE, JAIPH_AGENT_CURSOR_FLAGS, JAIPH_AGENT_CLAUDE_FLAGS, JAIPH_RUNS_DIR, JAIPH_DEBUG.

Import trust boundary

agent.command and agent.backend are execution-binary keys — they determine which process runs prompt steps. To prevent a third-party .jh library from silently redirecting execution to a different binary, these two keys may only be set from the entry module’s config {} block (module-level or def-level). Imported modules that declare agent.command or agent.backend in their config {} are silently ignored for these keys.

agent.trusted_workspace, agent.cursor_flags, agent.claude_flags, and run.logs_dir carry the same entry-module-only restriction. They shape the agent trust path (passed to Cursor as --trust), the agent argv (cursor_flags / claude_flags are appended to the invocation), and the run directory — so an imported module setting them could redirect trust, alter the agent command line, or move run logs for its own prompt steps. Imported modules that declare these keys in their config {} are silently ignored for them.

Host secrets carry a related boundary: a script — imported or local — receives a host key only through its own use clause and an explicit operator --env grant, so a library cannot pull host secrets into its steps without the operator naming each key on the command line (see Script env keys).

The remaining config keys (agent.model, run.debug) are not restricted and follow the normal scoping rules for cross-module calls.

Advanced unlock (use with caution): to allow an imported module to override an entry-module-only key, set the matching environment variable before the run:

Variable Effect
JAIPH_AGENT_COMMAND_IMPORT_UNLOCK=1 Allow any imported module to set agent.command.
JAIPH_AGENT_BACKEND_IMPORT_UNLOCK=1 Allow any imported module to set agent.backend.
JAIPH_AGENT_TRUSTED_WORKSPACE_IMPORT_UNLOCK=1 Allow any imported module to set agent.trusted_workspace.
JAIPH_AGENT_CURSOR_FLAGS_IMPORT_UNLOCK=1 Allow any imported module to set agent.cursor_flags.
JAIPH_AGENT_CLAUDE_FLAGS_IMPORT_UNLOCK=1 Allow any imported module to set agent.claude_flags.
JAIPH_RUNS_DIR_IMPORT_UNLOCK=1 Allow any imported module to set run.logs_dir.

The matching ${NAME}_LOCKED=1 flag still applies on top — a locked key cannot be changed regardless of the source.

Config-to-env mapping

In-file key Environment variable
agent.model (prompt-scoped only — does not set JAIPH_AGENT_MODEL)
agent.command JAIPH_AGENT_COMMAND
agent.backend JAIPH_AGENT_BACKEND
agent.trusted_workspace JAIPH_AGENT_TRUSTED_WORKSPACE
agent.cursor_flags JAIPH_AGENT_CURSOR_FLAGS
agent.claude_flags JAIPH_AGENT_CLAUDE_FLAGS
run.logs_dir JAIPH_RUNS_DIR
run.debug JAIPH_DEBUG
run.recover_limit (no env override)
module.name (no env override)
module.version (no env override)
module.description (no env override)

Variables with no in-file equivalent: JAIPH_CODEX_API_URL (codex endpoint), JAIPH_PROMPT_RETRY / JAIPH_PROMPT_RETRY_DELAYS (prompt-retry schedule), OPENAI_API_KEY (codex credential). Full inventory in Environment variables.

Backend selection

Backend Required credential Endpoint / CLI
cursor (default) CURSOR_API_KEY (or stored cursor-agent login on host runs) Runs agent.command (default cursor-agent) with stream-json framing.
claude ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN (or stored Claude CLI login on host runs) Runs claude on PATH.
codex OPENAI_API_KEY Calls the OpenAI Chat Completions endpoint directly (JAIPH_CODEX_API_URL overrides the URL). No CLI-login fallback.

Backend-specific flags come from agent.cursor_flags / agent.claude_flags (or the matching env vars). There is no per-prompt backend override.

Credential pre-flight

Before jaiph run spawns the runner, the host CLI runs a credential pre-flight (src/cli/run/preflight-credentials.ts). It collects the distinct backend(s) declared in the entry file’s module-level config block and each def-level block, plus the effective default (JAIPH_AGENT_BACKEND env, or cursor when unset). Deeper per-import overrides resolved at runtime are not followed.

Backend Required credential Host behaviour
codex OPENAI_API_KEY hard error (E_AGENT_CREDENTIALS)
claude ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN not checked (CLI login is the host path)
cursor CURSOR_API_KEY warn (CLI login may still work)

Hard errors exit non-zero with no runner launched. Warnings go to stderr and the run proceeds. Skip cases: entry file declares no explicit backend and uses no prompt step → no pre-flight; jaiph run --raw → no pre-flight.

Every error and warning names the backend, the model when agent.model is set, the entry .jh file, the config scope, and the concrete remedy. The config scope is one of module config, def <name>, JAIPH_AGENT_BACKEND env, or default.

Model resolution

Resolution order for a prompt step:

Step Source Notes
1 User env — JAIPH_AGENT_MODEL non-empty. model_reason: explicit. Applies to every prompt in the run.
2 In-file config — agent.model from def-level then module-level metadata (interpolated at prompt time). model_reason: explicit. Applies to that prompt invocation only; passed as --model to the backend CLI without writing JAIPH_AGENT_MODEL.
3 Flags model — --model <name> inside agent.cursor_flags / agent.claude_flags. model_reason: flags. Codex has no flag channel; this step does not apply.
4 Backend default — Cursor/Claude binaries pick their own. Codex defaults to gpt-4o in code. model_reason: backend-default.

For the Claude backend, when agent.model is set and agent.claude_flags does not already contain --model, Jaiph passes --model <value> to the Claude CLI automatically. When agent.claude_flags already contains its own --model, Jaiph does not add a second one, so the model in agent.claude_flags is the one the Claude CLI receives. When both are set this way, the PROMPT_START / PROMPT_END records still carry model_reason: explicit with the agent.model value, even though the Claude CLI ran with the model from agent.claude_flags.

PROMPT_START / PROMPT_END records in run_summary.jsonl carry model (resolved string, or null when the backend auto-selects) and model_reason (explicit, flags, backend-default, or none for a custom agent command, which has no model concept).

Prompt retry on transport failure

NodeWorkflowRuntime retries transport-failed prompt invocations on an escalating backoff schedule before propagating the failure to any enclosing recover / catch.

Attempt Delay before this attempt
1 (none — initial call)
2 15s
3 1m
4 10m
5 30m
6 2h

Total worst-case wall-clock: ~2h41m.

Only transport failures are retried (non-zero exit from cursor/claude, codex HTTP error, spawn failure). Deterministic post-processing failures — invalid JSON, schema validation — fail on the first attempt and return { ok: false }.

Each attempt emits its own PROMPT_START / PROMPT_END and STEP_START / STEP_END. Each failure logs a LOGERR line; the final termination logs another. The captured value reflects the successful attempt.

Variable Effect
JAIPH_PROMPT_RETRY=0 Disable retry entirely (one attempt, fail on transport failure).
JAIPH_PROMPT_RETRY_DELAYS Comma-separated list of non-negative integer milliseconds. Invalid entries abort the prompt.

jaiph test defaults JAIPH_PROMPT_RETRY=0. Backoff sleep is interruptible: run abort, SIGINT, or SIGTERM cancels the pending wait without further backend calls.

Prompt watchdog timeouts

The retry backoff above handles a backend that fails. A separate set of watchdogs handles a backend that hangs — it never exits, so without them the runtime would block on the subprocess indefinitely (no commit, no queue progress, no retry). Each prompt invocation installs three independent layers over the spawned backend process:

Layer Variable Default Trigger Outcome
Completion grace JAIPH_PROMPT_COMPLETION_GRACE_SECONDS 30 The backend emitted its terminal result event (work is done) but the process has not exited within the grace window. Terminate the process, return success with the captured answer.
Idle timeout JAIPH_PROMPT_IDLE_TIMEOUT_SECONDS 900 (15m) No stdout/stderr for the whole window — the backend is stuck mid-work. Terminate the process, return failure → feeds the retry backoff.
Absolute cap JAIPH_PROMPT_MAX_SECONDS 7200 (2h) Total wall-clock for the single invocation exceeds the cap, regardless of activity. Terminate the process, return failure → feeds the retry backoff.

Set any variable to 0 to disable that layer. The idle timer resets on every chunk of backend output, so a slow-but-active run is bounded only by the absolute cap.

The completion-grace layer specifically addresses the known claude -p failure mode where the CLI streams its final answer (and the terminal result event) but the process never exits — often because a descendant it spawned is still holding the output pipe open. When a watchdog fires it terminates the backend’s whole process tree (via killProcessTree; see Architecture) with SIGTERM, escalating to SIGKILL after 5s, and tears down the runtime’s handles on the child’s stdio so a lingering descendant cannot keep the run alive. On Windows the tree is force-killed with taskkill /T on the first signal, so the SIGKILL escalation is a no-op.

Overall run timeout and step cap

The prompt watchdogs above bound a single backend call. Jaiph also has two controls that bound the whole run, and both are off by default, so existing runs behave as before.

JAIPH_RUN_TIMEOUT sets a parent-enforced wall-clock cap, in seconds, for a run. Without this cap, the only automatic stop is a manual Ctrl-C, because the host spawn installs only SIGINT and SIGTERM handlers and the prompt watchdogs cover a single backend call. When the cap is reached, the parent terminates the run child’s whole process group with SIGTERM and escalates to SIGKILL after a short grace period (via killProcessTree; see Architecture), so the run stops without a manual Ctrl-C, and the failure footer shows E_RUN_TIMEOUT. Set it to 0, leave it empty, or give it an invalid value to disable it, which restores the earlier behaviour where only a manual SIGINT or SIGTERM stops a run.

JAIPH_MAX_STEPS sets an optional max-step circuit breaker in the runtime. When you set it to a positive integer, the runtime counts every executed step across the whole run, and it counts loop iterations and nested or recursive calls but skips trivia. Once the count goes past the cap, the runtime logs E_MAX_STEPS, aborts the run, and returns a failure, so a runaway program stops on its own without a manual signal. Set it to 0, leave it empty, or give it an invalid value to disable the breaker.

Leaf step idle output

The prompt watchdogs bound a single backend call, and the run timeout and step cap bound the whole run. A separate watchdog bounds one leaf step by how long it stays silent. A leaf step is a script step or a prompt step, and the watchdog watches its stdout and stderr. It catches a script step that the prompt watchdogs never see, such as an overnight npm run test:ci that stops producing output but never exits. The runtime checks for silence on a poll interval set by JAIPH_STEP_IDLE_WARN_CHECK_MS (default 5000 milliseconds, floor 250).

JAIPH_STEP_IDLE_WARN_SEC (default 180 seconds; 0 disables) sets how long a leaf step can go without any stdout or stderr before the runtime emits a LOGWARN. The warning repeats on the same cadence while the step stays silent, so at 180 seconds, then 360, then 540, and so on. Any new output resets the clock. The warning does not fail the step, and it only surfaces a stalled backend or a long-running command.

JAIPH_STEP_IDLE_KILL_SEC (default 3600 seconds, one hour; 0 disables) sets a hard limit for a script step. When a script step goes that long without any stdout or stderr, the runtime emits a LOGERR naming the step and how long it was silent, terminates the step’s subprocess with SIGTERM and then SIGKILL (via killProcessTreeEscalating; see Architecture), and fails the step. Any new output resets this clock too. The warn cadence and the kill limit run off the same idle clock but fire independently. The kill applies to script steps only, because a prompt step’s backend is already bounded by the prompt watchdogs above, so a prompt step still gets the idle warnings but is never killed by this limit.

Custom agent commands

agent.command is consumed by the cursor backend only. For claude and codex, Jaiph always invokes the Claude CLI or the codex HTTP path, regardless of agent.command.

When agent.backend = "cursor" and agent.command’s basename is anything other than cursor-agent, Jaiph treats it as a custom agent command:

Channel Behaviour
stdin Prompt text piped to the command.
stdout Captured as the prompt response (raw text, no JSON framing).
stderr Passes through to the terminal.
Cursor-specific flags (--output-format, --stream-partial-output, --workspace, …) Not appended.
Step display name Command basename instead of cursor.

Custom commands still participate in PROMPT_START / PROMPT_END, write artifacts, and apply returns schema validation when configured.

Test runner

jaiph test does not call resolveRuntimeEnv. The test runner spreads process.env, then sets JAIPH_TEST_MODE, JAIPH_WORKSPACE, JAIPH_RUNS_DIR (ephemeral), JAIPH_SCRIPTS, and mock fields (JAIPH_MOCK_RESPONSES_JSON, JAIPH_MOCK_PROMPT_ARMS_JSON). JAIPH_*_LOCKED flags are not set unless inherited from the parent env. NodeWorkflowRuntime still applies in-file config via applyMetadataScope with the same lock rules.

Inspecting effective config at runtime

Agent and run settings are visible inside defs and scripts as JAIPH_* environment variables. In orchestration strings, ${IDENT} resolves against def bindings first, then against the process environment.

Created by jaiph init

jaiph init creates .jaiph/bootstrap.jh, .jaiph/SKILL.md, and .jaiph/.gitignore. There is no separate config file — config { … } blocks live in program source. See CLI — jaiph init.