A Claude Code plugin that exposes a locally-hosted LLM (LM Studio, Ollama, any OpenAI-compatible endpoint) as a sandboxed sub-agent.
Parent Claude drives it through the agnz CLI (from Bash). The sub-agent does the heavy file work — reading, grepping, mechanical edits — and Parent Claude only sees the distilled outcome. Same value model as the built-in Agent tool, but the model is one you control and host.
- Save tokens. Use a free local model for grunt work instead of burning Anthropic credits on dozens of intermediate file reads.
- Keep Parent Claude's context small. Only the sub-agent's final answer ever lands in the parent transcript.
- Stay in control. The sub-agent runs inside a sandbox: locked to a single working directory, tiered permissions defined by the agent definition.
- Real concurrency, no daemon. Each run is its own short-lived OS process (a detached runner) — nothing stays resident between runs, and multiple agents run in genuine parallel.
There is no MCP server. The parent calls the CLI via Bash; every verb prints a JSON object/array to stdout so the outcome is parseable. When the plugin is enabled, Claude Code puts its bin/ on the parent shell's PATH, so the parent runs agnz <verb> from any working directory — no $CLAUDE_PLUGIN_ROOT needed. (Developing in this repo, node bin/agnz.mjs <verb> works too.)
| Verb | Purpose |
|---|---|
start <name> ["task"] --agent <def> |
Create a thread (--inline "<frontmatter>" for an ad-hoc role). Without a task it starts idle. |
send <name|id> "msg" |
Send a task. Reuses the existing live thread of that name (resume), else needs an id. |
approve <id> allow|deny [--persist] |
Resolve an approval pause (no tool_call_id needed — the thread's pending call is used). |
answer <id> "text" |
Resolve an AskUser question pause. |
interrupt <id> ["directive"] |
Hard-interrupt a runaway/working agent: aborts the current step, leaves it resumable, optionally queues a directive. |
stop <id> |
End a thread (kills its runner; transcript persists). |
list [--status <s>] [--all] |
List threads in this workspace. |
show <id> |
Thread state + last few transcript messages. |
Add --wait to start/send/approve/answer to run the segment synchronously and get the outcome inline (for short tasks). Otherwise the run is detached and the result reaches the parent via the message hook: the runner appends to messages.jsonl, and the UserPromptSubmit hook injects unread parent mail into your next prompt automatically (an OS notification fires for urgent mail). Anything else — inspecting transcripts — is a plain file read under <cwd>/.claude/agnz/, or the /agnz:threads skill.
Claude Code (Parent)
│ Bash
▼
bin/agnz.mjs ← CLI: start/send/approve/answer/stop/interrupt/list/show
│ spawns a detached runner per active run
▼
lib/runner.mjs → lib/loop.mjs ← LLM ↔ tool loop, persists transcript, then exits
│
├──▶ tools/ (Read, Edit, Write, Grep, LS, Bash, AskUser, SendMessage, Skill)
├──▶ sandbox.mjs (cwd lock + tiered permission policy)
├──▶ agent-defs.mjs (named roles from .claude/agents/ and plugin agents/)
├──▶ workspace-store (<cwd>/.claude/agnz/ — threads, workspace.json)
├──▶ thread-index (user-wide id → cwd map)
├──▶ proc-lock.mjs (cross-process mkdir locks on shared state files)
├──▶ profiles.mjs (named LLM endpoint configs, user-wide)
└──▶ llm/openai-compatible.mjs (native fetch, no SDK)
Results flow back independently of the CLI process via messages.jsonl + the UserPromptSubmit hook. For the deep dive — module map, agent loop, sandbox semantics — see CLAUDE.md and ADR 0014.
Each turn the agent receives a system prompt composed of:
- Sandbox framing — cwd, tool workflow rules (Grep before Read, Read before Write — ADR 0013), messaging instructions
- CLAUDE.md files —
<cwd>/CLAUDE.mdat startup; subdirectoryCLAUDE.mdfiles are added as the agent accesses files in those directories - Tool restrictions — which tools are allowed/denied per the agent def
- Skills catalog — names + descriptions of available skills; agent loads full content on demand via
Skill({action:"load", name:"..."}) - Agent body — the role definition from the agent def frontmatter
This repo is a plain Claude Code plugin. The canonical marketplace is Superheld/claude-bauchladen:
/plugin marketplace add Superheld/claude-bauchladen
/plugin install agnz@claude-bauchladen
/reload-plugins
Installing wires the hooks (result delivery + spend summary), the bundled agents, and the skills. The parent invokes the CLI via Bash; verify with:
agnz listAfter code changes, update in place: /plugin marketplace update agnz && /plugin install agnz@agnz && /reload-plugins.
Run /agnz:setup add for interactive setup, or pass all fields directly. For LM Studio (default endpoint http://localhost:1234/v1):
node ${CLAUDE_PLUGIN_ROOT}/skills/agnz-setup/scripts/companion.mjs setup add \
lmstudio-devstral \
http://localhost:1234/v1 \
mistralai/devstral-small-2-2512Profile resolution at thread start: workspace.json → modelProfileMappings[model] → fallback to _default. Configure with /agnz:setup.
Any Claude Code agent .md file works as an agnz agent definition. Pass --agent <name> to agnz start; the def is resolved from <cwd>/.claude/agents/, ~/.claude/agents/, then the plugin-bundled agents/ directory (first match wins).
Plugin-bundled defaults: dev, researcher, reviewer, general.
The tools: frontmatter field lists tools that run without approval. Anything not listed requires agnz approve. disallowedTools: blocks tools entirely.
Two independent roots:
User-wide — profiles and cross-project index. Default: ~/.claude/agnz/. Override with $AGNZ_DATA_DIR.
~/.claude/agnz/
├── profiles.json
└── thread-index.json ← thread_id → cwd map
Per-project — one workspace per cwd, co-located with other Claude Code state:
<cwd>/.claude/
├── agents/
│ └── <name>.md ← agent definitions (shared with CC)
├── agnz/
│ ├── workspace.json ← shared metadata + modelProfileMappings
│ ├── messages.jsonl ← event bus for inter-agent communication
│ ├── cursors/ ← parent read-cursor state (hook delivery tracking)
│ └── threads/
│ ├── <thread-id>.meta.json
│ ├── <thread-id>.jsonl
│ └── <thread-id>.trace.jsonl
└── skills/
└── <name>/
└── SKILL.md ← project-local skills
| Skill | Slash command | Purpose |
|---|---|---|
agnz-setup |
/agnz:setup |
Manage LLM profiles (add, remove, use, test, mappings) |
agnz-threads |
/agnz:threads |
List and inspect threads in the current workspace |
agnz |
— | Progressive-disclosure reference for agent definitions and the CLI lifecycle |
Every thread writes an append-only runtime trace next to its transcript
(<thread-id>.trace.jsonl): per-turn LLM latency + token usage, tool outcomes,
JSON-repair events, and a terminal thread_end. From it you get:
- Stats —
node ${CLAUDE_PLUGIN_ROOT}/lib/trace-stats.mjs [<thread-id>](orinspect.sh stats) folds the trace into turns, tokens, latency, tool-error and repair rates, with per-model rollups. - Live spend — the
SessionStart/UserPromptSubmithooks inject a per-thread spend line (dev:1a2b3c4d — running · 5 turns · 1,234 tok) into Claude's context, so the parent sees what's running without reading files. - Evals —
node evals/run.mjsruns fixtures against one or more profiles in throwaway workspaces and scores outcome + trace metrics, ranking profiles by pass rate then token cost. The answer to "which local model for which role?".
Tests: node --test tests/*.test.mjs (the loop runs against an injectable fake LLM, no
model needed). Full guide: docs/observability.md.
- Native Node only. No npm dependencies.
- Comments explain why, not what.
- JSONL for streams, JSON for snapshots. Transcripts are append-only; thread meta is rewritten in place.
- Two data roots, two lifetimes. User-wide under
~/.claude/agnz/for cross-project state. Per-project under<cwd>/.claude/agnz/for workspace state.
Design decisions are captured as ADRs under docs/adr/ — authoritative descriptions of how the system works right now. The CLI architecture is ADR 0014; observability/evals, context management, and tool-workflow discipline are ADRs 0011–0013.
MIT