jinn is a sandboxed tool executor for AI coding agents. You pipe it a JSON request, it runs one tool, and it prints a JSON response. It ships as a single binary with no external runtime services.
Install with Go:
go install github.com/dotcommander/jinn/cmd/jinn@latestOr build from source:
git clone https://github.com/dotcommander/jinn.git
cd jinn
go build -o jinn ./cmd/jinn/jinn --versionYou'll see the version derived from your VCS tag or module path. To dump all tool definitions as JSON:
jinn --schemaThis prints the full compact OpenAI function-calling schema for every tool jinn exposes. For in-protocol discovery, list_tools returns compact capability metadata by default and includes the schema only when requested.
jinn reads exactly one JSON object of at most 16 MiB from stdin and writes one JSON object to stdout. Duplicate keys, trailing values, unknown fields, invalid types, and unknown tool arguments are rejected. Read a file:
echo '{"tool":"read_file","args":{"path":"go.mod"}}' | jinnYou'll get a response like:
{
"ok": true,
"result": "1\tmodule github.com/dotcommander/jinn\n2\t\n3\tgo 1.26.4\n"
}If something goes wrong, ok is false and the response contains an error field:
{
"ok": false,
"error": "file not found: nonexistent.txt"
}Every request follows this shape:
{
"tool": "tool_name",
"args": { }
}At minimum, every response has one of these shapes:
{"ok": true, "result": "..."}
{"ok": false, "error": "..."}Some tools add optional fields to the envelope:
| Field | When present | Description |
|---|---|---|
content |
Structured content responses | Typed blocks such as detected images from read_file |
meta |
Structured metadata responses | Checksums, truncation details, diffs, stdout/stderr, and compression details |
error_code |
Structured errors | Stable error category for programmatic handling |
suggestion |
On structured errors | One-sentence next-step hint — read it before retrying |
classification |
run_shell (always) |
Exit-code class: success, expected_nonzero, error, timeout, signal |
risk |
run_shell (always) |
Pre-execution risk: safe, caution, dangerous |
request_id |
When supplied by caller | Echoes the top-level request ID |
Example with extended fields:
{"ok": true, "result": "[exit: 0]\nok\n[classification: success — exit 0]", "risk": "safe", "classification": "success"}The protocol is one-shot: one JSON request on stdin, one JSON response on stdout. jinn is not a daemon. It starts, handles one request, and exits.
If stdin is a terminal (you run jinn with no pipe), jinn prints a short help message and exits. You must pipe input or redirect from a file.
result=$(echo '{"tool":"read_file","args":{"path":"main.go"}}' | jinn)
ok=$(echo "$result" | jq -r '.ok')
if [ "$ok" = "true" ]; then
echo "$result" | jq -r '.result'
else
echo "failed: $(echo "$result" | jq -r '.error')"
fiimport json, subprocess
def jinn(tool: str, args: dict) -> dict:
req = json.dumps({"tool": tool, "args": args})
proc = subprocess.run(
["jinn"],
input=req,
capture_output=True,
text=True,
timeout=60,
)
return json.loads(proc.stdout)
# Read a file
resp = jinn("read_file", {"path": "go.mod"})
if resp["ok"]:
print(resp["result"])
else:
print(f"error: {resp['error']}")async function jinn(tool: string, args: Record<string, unknown>) {
const proc = Bun.spawn(["jinn"], {
stdin: new TextEncoder().encode(JSON.stringify({ tool, args })),
stdout: "pipe",
});
return JSON.parse(await new Response(proc.stdout).text());
}
const resp = await jinn("read_file", { path: "go.mod" });
console.log(resp.ok ? resp.result : `error: ${resp.error}`);package main
import (
"bytes"
"encoding/json"
"fmt"
"os/exec"
)
func jinn(tool string, args map[string]any) (map[string]any, error) {
req, _ := json.Marshal(map[string]any{"tool": tool, "args": args})
cmd := exec.Command("jinn")
cmd.Stdin = bytes.NewReader(req)
out, err := cmd.Output()
if err != nil {
return nil, err
}
var resp map[string]any
if err := json.Unmarshal(out, &resp); err != nil {
return nil, err
}
return resp, nil
}
func main() {
resp, err := jinn("read_file", map[string]any{"path": "go.mod"})
if err != nil {
panic(err)
}
fmt.Println(resp["result"])
}<?php
function jinn(string $tool, array $args): array {
$req = json_encode(['tool' => $tool, 'args' => $args]);
$proc = proc_open('jinn', [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
], $pipes);
fwrite($pipes[0], $req);
fclose($pipes[0]);
$out = stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($proc);
return json_decode($out, true);
}
$resp = jinn('read_file', ['path' => 'go.mod']);
echo $resp['ok'] ? $resp['result'] : "error: {$resp['error']}";while IFS= read -r line; do
echo "$line" | jinn
done < requests.jsonlEach line in requests.jsonl is a complete JSON request object. jinn processes them one at a time.
| Flag | Description |
|---|---|
--schema |
Print all tool definitions as JSON and exit |
--inspect [addr] |
Start the local browser inspector UI. Defaults to 127.0.0.1:8787 |
--mcp |
Start the MCP 2026-07-28 stdio broker in the route-only discovery profile |
--mcp-http [addr] |
Start the stateless MCP 2026-07-28 Streamable HTTP broker at /mcp. Defaults to 127.0.0.1:8788 |
--mcp-profile=discover|read-only|network |
Select the MCP profile. read-only adds guarded local inspection; network adds guarded web execution. Requires --mcp or --mcp-http |
JINN_MCP_HTTP_TOKEN |
Optional loopback bearer token, required for non-loopback HTTP binds |
JINN_MCP_HTTP_ORIGINS |
Exact comma-separated HTTP(S) origins, required for non-loopback HTTP binds |
--version |
Print the version and exit |
--help, -h |
Print usage information and exit |
jinn web fetch --json https://example.com
jinn web fetch --json --max-lines=200 https://example.com/article
jinn web search --provider=exa --include-domain=go.dev "context cancellation"These subcommands use stdlib flags with human output and flat --json envelopes. --max-bytes and --max-lines add an explicit human truncation marker or JSON byte/line totals and truncation fields. Reader/cache/render settings use JINN_WEB_*, including JINN_WEB_CHROME_PATH; provider credentials use JINA_API_KEY, BRAVE_API_KEY, or EXA_API_KEY. The default cache directory is $XDG_CACHE_HOME/jinn/web/urls, falling back to ~/.cache/jinn/web/urls.
web_fetch and web_search are available to normal one-shot discovery. Native web_fetch accepts format=markdown|headings|links, zero-based start_line, max_bytes, and max_lines; continuation and truncation details are returned in snake-case metadata. They are network-risk tools, excluded from read-only, and rejected from every run_plan phase. Use --mcp-profile=network only when the MCP host is allowed to make public network requests.
jinn --mcp uses the official Go MCP SDK and newline-delimited JSON-RPC over
stdin/stdout. The current protocol is 2026-07-28 and uses stateless requests,
not an initialize handshake. Each request carries the protocol version and
client capabilities under _meta.
When an MCP host accepts only an executable path, it may launch jinn without
arguments. Jinn auto-detects MCP JSON-RPC on piped stdin and otherwise retains
the one-shot {tool,args} protocol. Interactive jinn still prints help.
The default broker exposes one MCP tool, jinn_route. The tool is
recommendation-only: it does not execute read_file, run_shell, or any other
jinn tool. It maps a natural-language need to the most relevant existing jinn
tools, risk/mutation notes, and optional lean schemas for only the matched
tools. Keeping one tool in the default MCP surface avoids prompt bloat from
listing all 21 executor schemas.
For agents that need bounded inspection without granting mutation or shell access, use the opt-in read-only profile:
{
"mcpServers": {
"jinn-read-only": {
"command": "jinn",
"args": ["--mcp-profile=read-only", "--mcp"]
}
}
}This profile exposes jinn_route and jinn_call. jinn_call dispatches only
the canonical read-only tool allowlist, while file/state mutation, shell
execution, memory, and undo are rejected before dispatch. It also forces
shell execution off. The profile uses the current stateless 2026-07-28 request
metadata; older initialize-based clients remain on the route-only compatibility
path.
The network profile retains only these two MCP tools and adds web_fetch and
web_search through jinn_call. Those requests leave the machine and may
consume provider quota.
Use the built-in explorer for local fakes, stdio subprocesses, or HTTP(S):
jinn mcp list ENDPOINT
jinn mcp inspect --command jinn --arg --mcp jinn_route
jinn mcp call ENDPOINT jinn_route -a need '"read Go files"'The deadline defaults to 30s; bearer auth defaults from JINN_MCP_HTTP_TOKEN.
Tool isError is a successful explorer response; transport/protocol failures
use Jinn's normal nonzero error envelope.
Use the separate opt-in flag when the client connects to a URL instead of a stdio process:
jinn --mcp-http
# POST http://127.0.0.1:8788/mcpThe default HTTP profile exposes only jinn_route. Add
--mcp-profile=read-only to expose the same guarded jinn_call profile
described above. HTTP requests are stateless and use one JSON-RPC request per
POST. They must include Accept: application/json, text/event-stream,
Content-Type: application/json, MCP-Protocol-Version: 2026-07-28, and
Mcp-Method; tools/call also needs
Mcp-Name.
Loopback binds work without environment controls. Any non-loopback bind is
rejected unless both JINN_MCP_HTTP_TOKEN and
JINN_MCP_HTTP_ORIGINS are set. The token is never placed in CLI arguments or
startup output, and the origin list uses exact HTTP(S) origins:
JINN_MCP_HTTP_TOKEN="$TOKEN" \
JINN_MCP_HTTP_ORIGINS="https://agent.example.com" \
jinn --mcp-profile=read-only --mcp-http 0.0.0.0:8788Use a non-loopback bind only on a trusted network or behind a TLS-terminating proxy or tunnel: bearer tokens authenticate requests but do not encrypt them.
HTTP deliberately does not accept the stdio legacy initialize handshake.
See mcp-smoke-test.md for a
real subprocess check of the endpoint, headers, auth, origin, and shutdown
contracts.
For migration safety, the default stdio profile detects a first older
initialize request and serves the legacy compatibility path. The read-only
profile deliberately leaves legacy traffic to the current SDK, which rejects it
before any route or tool dispatch. Current clients should use the 2026-07-28
request shape shown in tool-reference.md.
Example client config:
{
"mcpServers": {
"jinn": {
"command": "jinn",
"args": ["--mcp"]
}
}
}Routing is deterministic and local; weak matches are dropped rather than
guessed, and a vague need returns a corrective note. See
tool-reference.md for the input table, a
captured response, and need phrasing guidance.
The memory tool stores key/value pairs across jinn invocations in a SQLite database at ~/Library/Application Support/jinn/memory.db on macOS (~/.config/jinn/memory.db on Linux; override base dir with JINN_CONFIG_DIR), scoped per project:
# Save a value
echo '{"tool":"memory","args":{"action":"save","key":"db_url","value":"postgres://localhost/myapp"}}' | jinn
# Recall it later
echo '{"tool":"memory","args":{"action":"recall","key":"db_url"}}' | jinn
# List all keys
echo '{"tool":"memory","args":{"action":"list"}}' | jinn
# Save to the cross-project "global" scope
echo '{"tool":"memory","args":{"action":"save","key":"editor","value":"nvim","scope":"global"}}' | jinnBy default, keys are scoped to the current project (auto-detected from the nearest .git ancestor). Pass scope: "global" for a cross-project bucket, scope: "project" with scope_id for an explicit project path, or scope: "task" / scope: "agent" with a caller-supplied scope_id.
Keys must match [a-zA-Z0-9_.-] (max 128 chars). Values are capped at 16 KiB. Saved entries can also carry kind: "fact" | "directive" | "lesson", pin: true, and expires_in durations such as "12h", "7d", or "2w".
# Include values and metadata when listing
echo '{"tool":"memory","args":{"action":"list","include_values":true}}' | jinn
# Save a task-scoped entry that expires
echo '{"tool":"memory","args":{"action":"save","key":"backfill.lesson","value":"retry failed rows after the second pass","kind":"lesson","scope":"task","scope_id":"backfill-2026-06","expires_in":"14d"}}' | jinn
# Remove expired, unpinned memories and old idempotency rows
echo '{"tool":"memory","args":{"action":"gc"}}' | jinnThe lsp_query tool connects to a running language server to answer semantic questions about source code. The server is auto-selected from the file extension:
# Jump to definition at line 12, character 5
echo '{"tool":"lsp_query","args":{"action":"definition","path":"main.go","line":12,"character":5}}' | jinn
# List all symbols in a file
echo '{"tool":"lsp_query","args":{"action":"symbols","path":"internal/jinn/engine.go"}}' | jinn
# Pull diagnostics for a file
echo '{"tool":"lsp_query","args":{"action":"diagnostics","path":"main.go"}}' | jinnSupported actions: definition, references, hover, symbols, diagnostics, and rename preview. Supported extensions include Go, Rust, Python, TypeScript/JavaScript, C/C++, Java, Lua, and Zig. The server binary must be on PATH; if missing, the response includes a suggestion with the install command.
- Harness Integrations -- recipes for Claude Code, Codex CLI, and custom agent loops
- Tool Reference -- every tool with full parameter tables and examples
- Security -- rooted confinement, mutation preconditions, shell modes, and the risk classifier