picoclaw — the Go AI agent — wrapped as a Bare library, spoken to entirely over RPC.
State lives in a Hyperbee. The Go side never touches your terminal.
🐻 A Claude experiment. Built by Claude (Opus 4.8), spelunking the Holepunch + picoclaw stack.
bareclaw takes picoclaw (a capable, multi-provider AI agent written in Go) and exposes it as a clean, embeddable Bare module.
The trick: the Go agent is compiled to a small binary, spawned as a subprocess, and driven purely over bare-rpc across stdio using compact-encoding frames. From JavaScript you get a tidy async/streaming API; under the hood it's a Go agent loop.
Sessions and conversation history are persisted into a Hyperbee on the Bare side. The Go process is the stateless engine; the bee is the source of truth. Pass it a Corestore and it does the rest.
┌────────────────────────┐ bare-rpc over stdio ┌──────────────────────┐
│ Bareclaw (JS / Bare) │ ─── compact-encoding frames ───▶ │ picoclaw (Go binary)│
│ • Hyperbee state │ ◀── tool calls / chat stream ─── │ • agent loop │
│ • tool handlers (JS) │ │ • LLM providers │
└────────────────────────┘ └──────────────────────┘
npm install
Prebuilt Go binaries for darwin/linux/win32 × x64/arm64 ship in prebuilds/ (resolved via the #bareclaw import map). To rebuild them yourself:
make build # cross-compile all targets into prebuilds/
# or a single host build:
go -C go build -o ../prebuilds/darwin-arm64/bareclaw ./cmd
You'll also need an LLM the agent can reach — e.g. a local Ollama (ollama run llama3.2) or an API key for Anthropic/OpenAI/etc.
const Corestore = require('corestore')
const { Bareclaw } = require('bareclaw')
const store = new Corestore('./store')
await store.ready()
// All Go options are passed via `opts`.
const bc = new Bareclaw(store, { provider: 'ollama', model: 'llama3.2' })
await bc.ready()
// Sessions are keyed deterministically from their scope.
const key = await bc.session({ agentId: 'assistant', channel: 'general' })
// chat() yields structured, string-typed chunks.
for await (const chunk of bc.chat(key, 'Say hello in one word.')) {
if (chunk.type === 'content') process.stdout.write(chunk.content)
if (chunk.done) break // 'done' (or 'error') is a pure terminator
}
await bc.close() // flushes state to the bee, stops Go cleanly
Run the included demo with
bare example.js.
new Bareclaw(store, opts = {})store is a Corestore (the library opens its own Hyperbee on it). Everything Go needs comes through opts:
| option | type | notes |
|---|---|---|
provider | string | e.g. ollama, anthropic, openai |
model | string | e.g. llama3.2, claude-... (sets picoclaw model_name + model) |
apiKey | string | for hosted providers |
apiBase | string | custom endpoint |
config | object | string | inline picoclaw config (merged over defaults) — or a path to a config file |
builtinTools | boolean | keep picoclaw's built-in OS tools (default off — see below) |
Tools. By default a bareclaw agent has no built-in tools — its tools come from registerTool. picoclaw's built-in OS tools (file/exec/skills) act on the Go process, not your app, and make small models emit tool-call noise, so they're off unless you pass builtinTools: true.
await bc.session(scope = {})Returns a deterministic string key for a scope ({ agentId, channel, account, peer }). Same scope → same key. Records the session in the bee so it's listed and survives restarts.
await bc.sessions()Lists known session keys — read straight from the bee.
bc.chat(sessionId, message, opts = {}) → async iterableYields chunks shaped like:
{ type: 'content', content: '…', done: false } // streamed / final text
{ type: 'thinking', content: '…', done: false } // reasoning (when available)
{ type: 'done', done: true } // terminator, no body
{ type: 'error', content: '…', done: true } // terminator with message
opts.model overrides the model for a single turn. After each turn the session's history is persisted into the bee automatically.
await bc.exportSession(key) → BufferOpaque blob of a session's history + summary.
await bc.importSession(key, blob)Restores a session (into both Go and the bee). Round-trips with exportSession.
await bc.registerTool(name, description, schema, handler)Registers a JavaScript tool the Go agent can call. schema is JSON Schema; handler(input) returns the result. The agent invokes it via an RPC callback:
await bc.registerTool(
'reverse',
'Reverse a string',
{ type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
async ({ text }) => ({ result: [...text].reverse().join('') })
)
await bc.close()Flushes each session's final state to the bee, then shuts the Go process down gracefully (closes its stdin so it reaches EOF and exits).
Corestore/Hyperbee, they replicate, sync, and persist like any other Holepunch data structure.opts.Runnable P2P demos live in examples/ (each is bare examples/<file>):
| demo | what it shows |
|---|---|
swarm-collab.js | two agents discover each other on a Hyperswarm topic and collaborate by relaying turns |
peer-scan-tool.js | a registerTool P2P tool — the agent joins a swarm, counts peers, and leaves |
dht-shared.js | a HyperDHT server as a one-to-many hub: agents post ideas, the merged board fans back to all |
swarm-code-review.js | a multi-agent code-review panel with consensus — 2 agents per lens (to see agreement), a correlator weighs findings over the DHT, verdict written to review.md |
See examples/README.md for details.
npm test # brittle-bare test/all.mjs
Session/state tests run without an LLM. Chat and tool tests need a reachable model (the helpers default to ollama + llama3.2).
This is an experiment — here's what's rough or unfinished, honestly:
content frame rather than incrementally. The thinking chunk type is wired but rarely emitted.llama3.2 can still spontaneously emit function-call-shaped JSON or ramble. Use a capable model/provider for sharp, reliable output and real tool use.~/.picoclaw (or the configured dir). Fully routing that through the bee is future work.CMD_STATE_CHANGED push path exists but Go doesn't emit it yet; persistence happens after each chat turn and on close() instead.