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
$ 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 / command | Description |
|---|---|
--resume [id] | Continue the latest (or the given) session |
/model [spec] | Show or switch the model mid-session |
/undo | Revert the last jern-authored commit |
/clear | Forget this conversation's history |
/cost | Token totals for the session |
Ctrl-C interrupts the current turn; the conversation stays consistent.
jern run
$ jern run [--yes] [--agent <dir>] "task"
| Flag | Description |
|---|---|
--yes | Approve 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
| Command | Description |
|---|---|
jern undo | Revert the last jern-authored commit (exactly one; refuses anything else) |
jern eject | Copy the installed default agent's source into ./agents/default for editing |
jern repl | Kernel 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.
| Flag | Description |
|---|---|
--model provider/model | Route this run to a provider and model (providers) |
--auto | Auto-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|high | Reasoning 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": "…" } }
}
}
| Key | Description |
|---|---|
default_model | Used when no --model is given |
aliases | Short names for model specs |
providers | Custom or overridden endpoints (any OpenAI-compatible API) |
test_command | The default agent runs this after every edit and reacts to failures |
budget | Hard model-call / token caps, enforced in the handler stack |
thinking_tokens | Anthropic extended-thinking budget |
reasoning_effort | Effort for OpenAI-style reasoning models |
mcp_servers | MCP servers whose tools join the agent's toolset |
policy | Enforced 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:
--think <tokens>(or"thinking_tokens") turns on Anthropic extended thinking with that budget; the response's thinking blocks are preserved across turns, andmax_tokensgrows by the budget automatically.--effort low|medium|high(or"reasoning_effort") sets reasoning effort for OpenAI-style reasoning models (o-series, R1, …). The token cap is sent asmax_completion_tokensfor these models, and DeepSeek-stylereasoning_contentcomes back as a canonical thinking block.
$ 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:
- Interactive prompts answer
y(once),n(deny), ora— approve and stop asking about that tool for the session. The web UI's cards have the same three buttons. --auto(any command, and a live toggle in the UI) auto-approves everything the policy would ask about. Explicit policy denials still deny — they never reach an approver at all.
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.