Help us shape the future of Go! Take the 2026 Go Developer Survey

janus

module
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2026 License: MIT

README

Janus

release test Go licence OpenSSF Scorecard OpenSSF Best Practices

Documentation: risadams.github.io/Janus

Janus is a small proxy that sits on your machine between Claude Code and the Anthropic API. Out of the box it forwards every request untouched and writes down what each one cost. Everything else is a switch you turn on when you want it: sending some of the work to a local model in LM Studio, keeping secrets and private files on your machine, or keeping a session going after Claude goes down or your usage runs out.

flowchart LR
  cc["Claude Code"] -->|"every request"| janus["Janus<br/>127.0.0.1:8787"]
  janus -->|"most requests"| api["Anthropic API"]
  janus -.->|"what your rules send local"| lms["LM Studio<br/>local model"]
  cc -.->|"MCP tools"| mcp["janus mcp"]
  mcp -.-> lms
  janus --> logs[("logs/<br/>ledger and captures")]

While Claude Code works, Janus prints a line for each request: who answered, how long it took, how it ended, the tokens and how many came from the prompt cache, and what it would cost at list price. Here subagents are answered by a local model, and one local answer fails Janus's checks and goes to Claude instead:

Made-up requests against stand-in backends; reading the console explains each column.

Why

Claude Code is easy to run and hard to see into. A long session makes hundreds of requests, and nothing tells you which ones cost what. Janus keeps a ledger line for each, and janus stats adds them up by kind of request, model, and backend.

A local model can be tried without trusting it. In shadow mode it answers alongside Claude and only the answers are compared; in local-then-escalate mode it answers subagent turns, and Claude takes over whenever an answer fails its checks. Only once you are happy with it does it need to answer on its own.

Secrets can be replaced with placeholders before a request leaves and put back in the answer, and files you name never reach Anthropic at all. When Claude is unreachable or rate limited, which on a subscription is what running out of usage looks like, a rule can have the local model stand in.

Quick start

You need Claude Code. LM Studio is only needed for the local-model features. Before you start says what each feature needs and which settings differ from their defaults.

On macOS or Linux with Homebrew, for stable releases:

brew install risadams/tap/janus

brew upgrade then keeps it current, and the tab-completion scripts are installed with it. With Go 1.27 or later, install the newest release:

go install github.com/risadams/janus/cmd/janus@latest

Or download a build. This finds the newest release on the releases page, release candidates included, downloads the build for macOS on Apple silicon, and checks it against the published checksums:

url=$(curl -s https://api.github.com/repos/risadams/Janus/releases | grep -m1 -o 'https://[^"]*_darwin_arm64\.tar\.gz')
curl -LO "$url"
curl -LO "${url%/*}/checksums.txt"
shasum -a 256 -c checksums.txt --ignore-missing

checksums.txt is itself signed, so that a replaced release is caught too; SECURITY.md says how to check the signature with OpenSSL. Each archive also carries a signed record of the workflow and commit that built it. If you have the GitHub CLI, check that too, then unpack:

gh attestation verify "${url##*/}" --repo risadams/Janus
tar -xzf "${url##*/}"
cd janus_*_darwin_arm64

For another machine, swap darwin_arm64 for darwin_amd64, linux_amd64, or linux_arm64. On Windows, winget (winget install RisAdams.Janus) and Scoop (scoop bucket add risadams https://github.com/risadams/scoop-bucket, then scoop install risadams/janus) install stable releases; the Windows builds are also .zip files on the releases page. The macOS binaries are signed with a Developer ID and notarized by Apple, so macOS runs them however they were downloaded.

Check the setup, then start Janus (drop the ./ if you used go install):

./janus doctor
./janus

The first run writes janus.config.json in the current directory and prints the command that points Claude Code at Janus. Run it in another terminal:

ANTHROPIC_BASE_URL=http://127.0.0.1:8787 ENABLE_TOOL_SEARCH=true claude

Work as usual. Afterwards, see where the tokens went:

./janus stats -since 24h

The quick start guide carries on to sending your first requests to a local model, and the user guide covers installing on every platform, configuring, everyday use, and troubleshooting in full.

What happens to a request

flowchart TD
  req["Request from Claude Code"] --> policy{"Holds local-only<br/>content?"}
  policy -->|"withheld, or none"| plan{"Plan: model alias,<br/>override, or rule"}
  policy -->|"action: local"| local
  plan -->|"upstream"| prep["Repair history, apply transforms,<br/>replace secrets"]
  plan -->|"local or local-then-escalate"| local["Local model answers"]
  local --> check{"Answer usable?"}
  check -->|"no, or LM Studio down"| prep
  prep --> api["Anthropic API"]
  api -->|"down, and the rule has ifDown"| local
  check -->|"yes"| relay["Restore secrets,<br/>relay the answer"]
  api --> relay
  relay --> ledger[("Ledger line<br/>and capture")]

A request that the local-only policy keeps on your machine has no path to Anthropic: if the local model cannot answer it, Janus refuses it. Each step a request takes is recorded in the ledger, and what Janus changes lists them all.

Features

Everything except the ledger is off until you switch it on in janus.config.json.

Feature Switch What it does Guide
The books always on A ledger line per request, and full captures of requests and responses, kept 7 days Ledger
Routing features.route and routing rules Sends chosen requests to a local model, in shadow, local-then-escalate, or local mode Local first, subagents
Pacing a rule's ifUsageAbove Sends a rule's requests local once a subscription usage limit is nearly used, before Claude refuses Pacing
Outage stand-in a rule's ifDown The local model answers when Claude cannot. Janus loads it if needed, and cuts a conversation too long for it down to its start and latest turns Outage
Guard features.guard, guard.redact, guard.localOnly Finds secrets and swaps them for placeholders; keeps named files and markers away from Anthropic Guard, privacy
Worker tools janus mcp Lets Claude hand file reading to the local model, and fetch the originals of digested output Workers
Compression features.compress Replaces large tool results with digests written by the local model. Off by default, because it saved about 1.5% when measured Config, evaluation

The configuration reference has every setting.

Commands

Command What it does
janus Runs the proxy
janus doctor Checks the config, port, upstream, LM Studio, log directory, and whether Claude Code is using Janus, and says what to fix; -json
janus stats Tokens and estimated cost from the ledger; -since 24h, -json
janus audit What left this machine, request by request; -verify re-reads what was sent; -json
janus config upgrade Rewrites a config written for an older Janus in this version's form, keeping a copy
janus route Where a saved request would go, and why
janus replay Runs saved requests on the local model and compares its answers with Claude's
janus mcp The MCP server for Claude Code's worker tools
janus claude Starts Claude Code through Janus, after checking Janus is running
janus service Installs, removes, or checks Janus as a service that starts at log in (macOS, Linux)
janus version Prints the version; -check asks GitHub whether a newer release is out, -pre counts pre-releases
janus update Replaces a copy installed from a release with the newest one, after checking its signature
janus completion Prints tab completion for bash, zsh, fish, or PowerShell (install)

Each one takes -h.

Good to know

Your credentials pass through untouched, and Janus never stores or logs them. It listens on loopback only, and it refuses requests made by web pages.

Captures hold your prompts and code in full. Only your user can read them, and they are deleted after 7 days by default; set logging.capture to false to stop them altogether.

One thing changes on every Messages request: Janus asks Anthropic for gzip instead of brotli or zstd, so that it can read the answer.

Janus checks a local model's answers, but only for being broken: empty, cut off, unreadable, a refusal, or a call to a tool it was not offered. A well-formed wrong answer gets through. In the measurements, the one model tried on one Mac was good enough for subagent turns and nothing more.

The threat model covers the rest, and troubleshooting the common problems.

Status

Janus is in pre-release. Observing, routing, escalation, worker tools, compression, and the guard are built and tested. The 1.0 release waits on a report that measures them against the charter's goals over a week of everyday use. The roadmap has the milestones, and the research notes the measurements behind each decision.

Diagrams for use outside GitHub are in docs/diagrams.

Development

go test ./...

The tests run offline against fake servers. CONTRIBUTING.md says how changes are made here, and AGENTS.md has the full conventions, the opt-in tests against real traffic, and the rules that keep Janus safe to change. Report security problems as SECURITY.md describes.

License

MIT

Directories

Path Synopsis
cmd
janus command
Command janus is a proxy between Claude Code and the Anthropic API.
Command janus is a proxy between Claude Code and the Anthropic API.
internal
anthropic
Package anthropic holds the Messages API wire types: requests, content blocks, and stream events.
Package anthropic holds the Messages API wire types: requests, content blocks, and stream events.
backend
Package backend defines the interface every model backend satisfies, and its two implementations: the upstream Anthropic API and local LM Studio.
Package backend defines the interface every model backend satisfies, and its two implementations: the upstream Anthropic API and local LM Studio.
capture
Package capture writes the readable per-request logs: a Markdown render, the raw request and response, and the request as it was sent when Janus changed it.
Package capture writes the readable per-request logs: a Markdown render, the raw request and response, and the request as it was sent when Janus changed it.
config
Package config loads and validates the config file, layered with flags and environment overrides.
Package config loads and validates the config file, layered with flags and environment overrides.
digest
Package digest is the transform that replaces an oversized tool result, on its way upstream, with a digest the local model writes.
Package digest is the transform that replaces an oversized tool result, on its way upstream, with a digest the local model writes.
guard
Package guard keeps secrets and protected content from leaving the machine.
Package guard keeps secrets and protected content from leaving the machine.
guard/guardtest
Package guardtest holds the corpus of invented secrets that the guard's tests, and the leak tests, are run against.
Package guardtest holds the corpus of invented secrets that the guard's tests, and the leak tests, are run against.
instance
Package instance keeps to one Janus per log folder.
Package instance keeps to one Janus per log folder.
ledger
Package ledger writes one decision record per request to an append-only log, and computes the cache-weighted cost figures reported by janus stats.
Package ledger writes one decision record per request to an append-only log, and computes the cache-weighted cost figures reported by janus stats.
mcp
Package mcp is a small Model Context Protocol server: enough of the protocol to offer tools to a client over standard input and output.
Package mcp is a small Model Context Protocol server: enough of the protocol to offer tools to a client over standard input and output.
proxy
Package proxy is the HTTP front door.
Package proxy is the HTTP front door.
route
Package route names each request's class and decides which backend answers it: the upstream, or a local model by alias or by rule, with the fallbacks and checks a rule asks for.
Package route names each request's class and decides which backend answers it: the upstream, or a local model by alias or by rule, with the fallbacks and checks a rule asks for.
shadow
Package shadow runs a request a second time on the local model, after the real answer has gone out, and records how the two compare.
Package shadow runs a request a second time on the local model, after the real answer has gone out, and records how the two compare.
transform
Package transform rewrites the content of requests bound upstream, to send fewer tokens, without breaking prompt caching.
Package transform rewrites the content of requests bound upstream, to send fewer tokens, without breaking prompt caching.
update
Package update finds out whether a newer Janus has been released.
Package update finds out whether a newer Janus has been released.
validate
Package validate decides whether a local model's answer is fit to give to the client, or should be thrown away in favour of asking Claude.
Package validate decides whether a local model's answer is fit to give to the client, or should be thrown away in favour of asking Claude.
worker
Package worker holds the tools janus offers Claude for handing work to the local model: reading that would otherwise fill Claude's context, done locally, with only a short answer going back.
Package worker holds the tools janus offers Claude for handing work to the local model: reading that would otherwise fill Claude's context, done locally, with only a short answer going back.
scripts
archive command
Command archive packs a folder into a release archive that is the same bytes wherever and whenever it is made, so that anyone can rebuild a release from its tag and compare (#264, ADR-004).
Command archive packs a folder into a release archive that is the same bytes wherever and whenever it is made, so that anyone can rebuild a release from its tag and compare (#264, ADR-004).
checklayout command
Command checklayout checks that a release built by scripts/release.sh is laid out as every installed Janus expects (ADR-004): it serves the dist folder as GitHub would and runs janus update's own install code against it, for every platform, with the keys the checksums were signed with.
Command checklayout checks that a release built by scripts/release.sh is laid out as every installed Janus expects (ADR-004): it serves the dist folder as GitHub would and runs janus update's own install code against it, for every platform, with the keys the checksums were signed with.
consoleshot command
Command consoleshot makes the picture of the Janus console in the docs (#223): it runs a scratch Janus against stand-ins for the Anthropic API and LM Studio, sends it made-up requests shaped like Claude Code's, keeps the console's colored output, and draws it as an SVG terminal window.
Command consoleshot makes the picture of the Janus console in the docs (#223): it runs a scratch Janus against stand-ins for the Anthropic API and LM Studio, sends it made-up requests shaped like Claude Code's, keeps the console's colored output, and draws it as an SVG terminal window.
signsums command
Command signsums signs a release's checksums.txt, so that a download can be checked against a key compiled into Janus rather than against the release page it came from.
Command signsums signs a release's checksums.txt, so that a download can be checked against a key compiled into Janus rather than against the release page it came from.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL