jern documentation

The complete CLI reference. jern is a terminal coding agent whose loop, tools, and policies are IronKernel source you can read, edit, and regression-test — see the open-source page for the thesis and the security model for the honest fine print.

Install Quick start Commands Global flags Configuration Providers & models Reasoning models Approvals & policy Budgets MCP Subagents & memory Programs & skills Receipts Golden sessions & CI Testing agents The web UI

Install

$ curl -fsSL https://jern.ai/install.sh | sh

macOS and Linux, arm64 and x64. JERN_VERSION=0.14.0 pins an exact release (and JERN_REQUIRE_SUMS=1 makes a missing checksum file fatal — worth setting in CI). The script installs to ~/.jern/bin (override with JERN_INSTALL=…) and tells you the PATH line to add. Windows and manual installs: download an archive from the releases page, untar, and put jern on your PATH — the archive carries the binary plus its Kernel source (kernel/, agents/, ui/); all of it is the product.

Building from source needs the .NET 10 SDK and a sibling checkout of IronKernel; see the repository.

Quick start

$ export ANTHROPIC_API_KEY=…      # or OPENAI_API_KEY, or none for ollama
$ jern                            # interactive chat in this workspace
$ jern run "fix the failing test" # one-shot task; asks before writes/shell
$ jern ui                         # the same session as a local web app

Commands

jern

Interactive chat in the current workspace. History persists to .jern/sessions/<id>.json; a session survives interruption.

Flag / commandDescription
--resume [id]Continue the latest (or the given) session
/model [spec]Show or switch the model mid-session
/undoRevert the last jern-authored commit
/clearForget this conversation's history
/costToken totals for the session

Ctrl-C interrupts the current turn; the conversation stays consistent.

jern run

$ jern run [--yes] [--agent <dir>] "task"
FlagDescription
--yesApprove everything the policy would ask about (same as --auto)
--agent <dir>Run a different agent package (e.g. an ejected or custom agent)

Every run writes a JSONL effect trace to .jern/trace-*.jsonl.

jern ui

$ jern ui [--port n] [--agent <dir>]

The session as a local web app on 127.0.0.1, guarded by a startup token in the printed URL: streamed replies, tool chips with diffs, interactive approval cards, a brain editor with syntax highlighting and one-click test runs, and settings for models and API keys. See the web UI.

jern test

$ jern test [<agent-dir>] [--record]

Runs an agent package's test suite. Replay is deterministic and network-free; --record captures new fixtures from the live provider. See testing agents.

jern replay

$ jern replay <trace.jsonl> [--policy <file>] [--agent <dir>]

Re-runs a recorded session offline: the agent source executes for real, but model and tool effects answer from the trace — nothing touches the network or the workspace. A faithful replay completes silently; swap in a stricter policy with --policy (or an edited agent with --agent) to fork the recorded past and see the first effect where behavior would have diverged, recorded-vs-actual.

jern receipt

$ jern receipt                 # the newest run in this workspace
$ jern receipt .jern/trace-*.jsonl --md   # ready for a pull request

What a run did, re-derived from its trace. See receipts.

jern golden

$ jern golden record "task" [--slug <name>]
$ jern golden check [--filter <slug>] [--md]
$ jern golden list

Committed behavior snapshots, checked offline. See golden sessions & CI.

jern policy

$ jern policy                  # every rule in force, and where it came from
$ jern policy --show-compiled  # the Kernel source jern.json compiles to
$ jern policy init             # write .jern/policy.ikr for this workspace

jern mcp

Connects the configured MCP servers and lists their tools. See MCP.

jern undo · eject · repl · script

CommandDescription
jern undoRevert the last jern-authored commit (exactly one; refuses anything else)
jern ejectCopy the installed default agent's source into ./agents/default for editing
jern replKernel REPL inside the agent's restricted environment
jern script <file>Run a .ikr file as agent code under the full handler stack

Global flags

These work on any command.

FlagDescription
--model provider/modelRoute this run to a provider and model (providers)
--autoAuto-approve permissions that are not explicitly denied (approvals)
--budget <n>Hard cap the run at n model calls (budgets)
--think <tokens>Anthropic extended thinking with that budget (reasoning)
--effort low|medium|highReasoning effort for OpenAI-style reasoning models
--policy-baseline <file>Rules this checkout may tighten but not weaken (policy)
--policy-trust <sha256>Bless a policy's grants where jern must not prompt (CI)

Configuration

JSON, merged from ~/.config/jern/config.json then the workspace's jern.json (workspace wins per key):

{
  "default_model": "anthropic/claude-opus-5",
  "aliases": { "fast": "groq/llama-3.3-70b-versatile" },
  "providers": {
    "myproxy": { "base_url": "https://llm.corp/v1", "api_key_env": "MYPROXY_KEY" }
  },
  "test_command": "pytest -q",
  "budget": { "llm_calls": 30, "tokens": 200000 },
  "thinking_tokens": 8192,
  "reasoning_effort": "high",
  "mcp_servers": {
    "github": { "command": "npx",
                "args": ["-y", "@modelcontextprotocol/server-github"],
                "env": { "GITHUB_TOKEN": "…" } }
  }
}
KeyDescription
default_modelUsed when no --model is given
aliasesShort names for model specs
providersCustom or overridden endpoints (any OpenAI-compatible API)
test_commandThe default agent runs this after every edit and reacts to failures
budgetHard model-call / token caps, enforced in the handler stack
thinking_tokensAnthropic extended-thinking budget
reasoning_effortEffort for OpenAI-style reasoning models
mcp_serversMCP servers whose tools join the agent's toolset
policyEnforced repository rules — see approvals & policy

API keys come from provider environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY, …). Keys set in the UI can be persisted to ~/.config/jern/credentials.json (mode 0600); environment variables always win.

Providers & models

Models are named provider/model, aider-style. Built in: anthropic (native wire format) and, through one OpenAI-compatible layer: openai, ollama, openrouter, deepseek, groq, mistral, xai, gemini, lmstudio — plus anything you add under providers in jern.json. ollama and lmstudio need no key.

$ jern run --model anthropic/claude-opus-5 "…"
$ jern run --model ollama/qwen3 "…"
$ jern --model fast              # an alias from jern.json

Sessions, traces, and test fixtures use one canonical conversation format regardless of provider — recorded fixtures replay against any of them.

Reasoning models

Two knobs, one intent — the agent states it once and each provider bridge consumes what it understands:

$ jern run --think 8192 "untangle the circular import"
$ jern run --model deepseek/deepseek-reasoner --effort high "…"

Because requests are authored in agent source, the wiring is ~15 lines of the default agent's main.ikr — read it, change it.

Approvals & policy

The policy decides first — :allow, :ask, or an explicit denial with a reason the model sees. Only :ask reaches you:

Rules from jern.json

The common cases need no Kernel at all — a policy object in jern.json is enforced for every session in that repository:

"policy": {
  "edits_within": ["src/", "tests/"],   // writes outside these are denied
  "shell_allow":  ["pytest"],           // auto-allow these commands
  "allow":        ["mcp__github__get_issue"],
  "deny":         ["mcp__*"],           // `*` suffix wildcards
  "memory":       "ask"                // allow | ask | deny
}

Policy composes; it is not overwritten. Restrictions (edits_within, deny, memory: ask|deny) only ever tighten. Grants (shell_allow, allow, memory: allow) only relax the base rules. Decisions combine by severity — a denial beats ask beats allow — so nothing loaded later can turn a restriction's denial into an approval: not a grant, not a hand-written policy file. Every decision in the trace records which layer made it, and jern policy prints the whole composition with digests and trust status.

The two halves are trusted differently. Restrictions load on sight — a repository may lock its agents down without asking. Grants can loosen approvals, so a repo-supplied one is confirmed once, keyed by the SHA-256 of the policy's canonical JSON; declining drops the grants and keeps the restrictions. A malformed policy is a startup error, never a silent no-op.

Unattended and CI runs. --policy-baseline <file> supplies rules from outside the checkout — a base branch, or data a workflow owns — that the tree may tighten but never weaken, so a pull request cannot buy itself permission by rewriting its own jern.json or .jern/policy.ikr. Headless runs never prompt: --policy-trust <sha256> blesses a policy's grants, and without a pin the grants are dropped while the restrictions stay. A baseline may carry an environment object beside policy (since 0.15.0): services and hosts the host running the session provides. jern validates the keys it knows, applies none of it, and names a key it does not know instead of refusing the file.

Rules as Kernel source

Beyond what the config expresses, the rules are enforced Kernel source. The workspace can carry its own: jern policy init writes .jern/policy.ikr, which rebinds the base policy for every session in that repo (config restrictions still wrap it):

(define tool-policy
  (lambda (call)
    (cond ((path-within? call "src/") :allow)   ; scope edits
          ((command-is? call "pytest") :allow)  ; allowlist a command
          ((equal? (plist-get call :name) "read_file") :allow)
          (#t :ask))))

A workspace policy runs privileged, so jern shows it and asks you to trust it the first time it loads — and again whenever its content changes. Yes answers are remembered per path and content hash in ~/.config/jern/trusted.json; declining keeps the built-in rules. Every session that loads one announces it.

Approved shell commands also run write-confined by the OS where possible: sandbox-exec on macOS and bubblewrap on Linux mount the filesystem read-only with the workspace and temp bound back writable (reads and network stay open — see the security model). Where neither is available, jern warns once and approval is the only gate.

Budgets

--budget 20 (or the budget object in jern.json) is a hard cap enforced in the handler stack: the 21st model call becomes an approval question — grant another round or the run ends with a budget error. Token budgets account real usage from each response. Not advice to the model; a wall in front of it.

MCP

jern is an MCP client (stdio transport). Servers configured in jern.json contribute tools named mcp__<server>__<tool>, which pass through the same policy, approval, trace, and fixture layers as the built-ins — every MCP call is ask-gated until your policy allows specific ones. jern mcp connects the configured servers and lists what they offer.

Subagents & memory

Both are effects, so both cross the same traced, policed choke point as every tool call.

Subagents. (spawn-agent "task") forks a child session running the agent's own brain; (spawn-agent-named "docs" "task") runs a different agent (an installed one, or a workspace-relative package directory). The whole handler stack — policy, approvals, budgets, memory, trace — composes recursively onto the child: it shares the parent's model bridge, approver, and workspace, its trace lines land in the same JSONL tagged {"spawn":N,…}, and depth is capped host-side. The child's final text comes back as a tool-result-shaped plist (:content / :is_error).

Memory. (remember "key" "value") and (recall "key") persist strings across sessions in .jern/memory.json. Every access is traced (memory-remember / memory-recall events) and decided by the memory-policy hook — :allow by default; a workspace policy can rebind it to :ask or deny with a reason:

(define memory-policy
  (lambda (op key) (if (equal? op "remember") :ask :allow)))

Since 0.15.0 the model has the same door as tools: memory_read and memory_write take a key (and a value) and cross the same effects, so the memory policy, the approver, and the trace govern them exactly as they govern agent source; a denying policy refuses memory_write with a tool error the model can read.

Programs & skills

Programmatic tool calling. Instead of one tool call per round-trip, the model can submit a whole IronKernel program through the kernel_eval tool — composing many tool calls with real control flow in one step:

(define content (plist-get (call-tool "read_file" (list :path "notes.md")) :content))
(if (string-contains? content "TODO")
    (call-tool "edit_file" (list :path "notes.md"
                                 :old_string "TODO" :new_string "DONE"))
    "nothing to do")

Programs run in a persistent child of the agent environment — a REPL with state, so definitions survive across calls — under a freshly installed copy of the full handler stack. That's the point: every tool call inside a program is still individually policy-checked, approval-gated, budgeted, and traced. The policy allows kernel_eval itself by default because authority lives at the effects, not the code — a workspace policy can tighten that. Program errors come back as an ordinary tool error for the model to read and fix; a runaway program is cut off at a wall-clock cap (limits.eval_timeout_seconds, default 30) and loses all effect access. Recorded programs re-execute under jern replay, their inner effects answered from the trace.

Workspace skills. .jern/skills.ikr loads into the agent environment at session start — helpers that agent source and model programs can both call. Unlike the workspace policy it is unprivileged and needs no trust prompt: skills code runs where the model's code runs, and can only reach the world through the same policed effects.

Receipts

Every jern run ends with the evidence for what it did:

receipt · run 20260824-101530 · 2m 14s · ok
  model calls   4 (anthropic/claude-opus-5) · 18.2k in / 2.1k out · budget 4/20
  tools         read_file ×3 · grep ×2 · edit_file ×2 · shell ×1
  files touched src/parser.py · tests/test_parser.py   (2 jern commits, undo with `jern undo`)
  policy        7 allowed · 2 approved by you · 1 denied (edits are limited to src/)
  trace         .jern/trace-20260824-101530.jsonl

A receipt is a pure function of the trace — nothing accumulates it while the run happens — so jern receipt <trace.jsonl> reproduces it for any run you still have, and --md renders it for a pull request comment (--json for tooling). In chat, /receipt shows the session so far; jern ui shows the same card when a turn ends.

To make that honest, the trace is a versioned run record: a run-started event carries the schema version and what the run was configured with (model, agent, budget, policy layer digests), and one run-finished event carries status and duration. A trace missing them — an older recording, or a run cut short — is summarized as explicitly partial rather than filled in with guesses. Events this build does not know are ignored; a trace from a future schema version is refused with guidance instead of misread.

Golden sessions & CI

Record a real task once, then check it forever — offline, with no API key:

$ jern golden record "fix the failing test"   # runs it for real, keeps the trace
$ jern golden check                          # replays every recording; no model calls
$ jern golden list

The recording is committed under .jern/golden/. check replays it against the agent source and effective policy in force now: change a prompt, a policy, or the loop, and the check fails with the exact recorded-vs-actual difference. Re-recording blesses a deliberate change.

Bytes are only half of it. Each recording's sidecar can assert what the run meant, and those assertions are evaluated against the recording itself — so they survive a re-record:

{
  "task": "fix the failing test",
  "assert": {
    "edits_within": ["src/"],
    "no_tools": ["shell"],
    "max_files_edited": 2,
    "max_llm_calls": 6
  }
}

A check re-executes the agent against the recording's model and tool results, so it catches changes to the agent: its source, its configuration, the policy that governs it. It does not catch a change to a file the agent reads during the run — editing a CONVENTIONS.md that the agent loads with read_file changes what a live run would see, while the replay still answers that read from the recording. Re-record to capture it.

Three related things, kept distinct on purpose: jern replay is ad-hoc forensics over any past run; jern golden check is a committed snapshot for CI; and jern test is an authored suite with semantic trajectory assertions in Kernel.

In GitHub Actions

- uses: actions/checkout@v4
- uses: jern-ai/jern/action@v0.14.0
  with:
    version: "0.14.0"
    baseline-path: .jern/baseline.json

The action installs a pinned, checksum-verified release, prints the effective policy, runs the agent tests and the golden check, uploads the traces, and posts one pull-request comment. Its policy baseline is read from the base commit, never from the pull request's own checkout, so a diff cannot weaken the rules that judge it — and a baseline that exists only in the pull request is refused.

For live work, a maintainer can manually dispatch task text or an issue number under a protected baseline and Cloud-returned hard token cap. On success, the Action can preserve Jern's commits on an isolated branch and open a pull request; failed runs upload evidence but publish no code. Full notes, including the required workflow permissions and what this does not protect against, are in action/README.md.

Testing agents

An agent package's test/*.ikr holds (deftest …) forms; each runs in a fresh session and temp workspace, with model traffic answered from recorded fixtures:

(deftest "fixes a typo end to end"
  (setup-file "greeting.txt" "helo world\n")
  (with-fixtures "fixtures/fix-typo.json"
    (assert-equal "Fixed the typo." (run-agent "Fix the typo in greeting.txt"))
    (assert-file-contains "greeting.txt" "hello world")
    ; …and properties of the whole run:
    (assert-no-tool-call "shell")
    (assert-edits-within "greeting.txt")
    (assert-max-llm-calls 3)
    (assert-tokens-within 50000)      ; input+output, from :usage
    (assert-max-files-edited 1)))     ; blast radius

Replay compares each request byte-for-byte with the recording and fails on the first divergence — a changed system prompt, reordered message, or different tool wiring is a test failure, offline, with no API key. The trajectory assertions add behavioral contracts on top; beyond the built-ins there's (assert-trajectory pred message) — any cross-turn invariant as a predicate over every traced event, failing with the first violation. Re-record with jern test --record to bless a deliberate change.

The web UI

jern ui serves a single page from the binary on 127.0.0.1. Every request needs the startup token from the printed URL, so other local processes can't drive the session. From the page you can: chat with streamed replies and expandable tool chips; answer approval cards (approve / always / deny) or flip the auto toggle; open the brain drawer to edit the agent's sources and the workspace policy with Kernel syntax highlighting, saving mid-conversation; run the agent's regression suite with one click; and manage models and API keys in settings. The page itself is ui/index.html beside the binary — edit it like everything else.