* feat(snip): implement HISTORY_SNIP — model-callable snip tool for context management
- snipProjection.ts: boundary detection + view filter (isSnipBoundaryMessage, projectSnippedView)
- snipCompact.ts: pending registry, snipCompactIfNeeded, shouldNudgeForSnips, SNIP_NUDGE_TEXT
- SnipTool/: model-callable snip tool with Zod schema (prompt.ts + SnipTool.ts)
- types/message.ts: add SystemCompactBoundaryMessage export
- scripts/build.ts: enable HISTORY_SNIP: true
- QueryEngine.ts: fix snipReplay return type
* docs: add MCP_SKILLS implementation plan
* docs: add HISTORY_SNIP implementation plan
* fix(snip): prune headless store on snip-boundary replay
The snipReplay path called snipCompactIfNeeded with a {force:true} option
that the function never read, and the pending-snip set was already cleared
when the boundary was produced in query.ts — so the replay always reported
nothing removed and mutableMessages never shrank in long SDK sessions.
Prune the store by the boundary's own removedUuids via projectSnippedView
instead. Also drop two planning docs that were committed to the branch.
* fix(snip): persist snip boundary in SDK/headless transcripts
When a snip boundary was yielded in the SDK/headless path, snipReplay pruned
the in-memory mutableMessages store but the branch broke before adding the
boundary to the local messages array or calling recordTranscript. Later
transcript writes used the pre-snip messages copy, so the on-disk transcript
kept the removed messages and no snipMetadata boundary. After a restart or
--resume, loadTranscriptFile reconstructed the un-snipped history and the
context reduction was lost.
Mirror the boundary into the local messages copy and record it when the snip
executes, matching the compact_boundary path. recordTranscript is append-only
by UUID, so the pre-snip messages already on disk remain and the appended
boundary (carrying snipMetadata.removedUuids) lets applySnipRemovals prune
them on load.
Add a loadTranscriptFile round-trip test covering the previously-untested
snip replay: a persisted boundary prunes its removedUuids and relinks
survivors whose parentUuid pointed into the removed gap.
* fix(history-snip): record paired tool-result removals and scope pending snips per conversation
Two issues in the snip path:
1. Persist every removed message. snipCompactIfNeeded drops the paired
tool-result user messages of a snipped assistant tool-use message from the
live context, but the boundary only recorded the explicitly-marked UUIDs.
projectSnippedView / loadTranscriptFile replay solely from
snipMetadata.removedUuids, so on --resume the tool results came back orphaned
(their assistant message stayed removed) and part of the reduction was lost.
Record the paired tool-result UUIDs in removedUuids so replay drops the same
set the live snip dropped.
2. Scope pending snips per conversation. The pending registry was module-global
and stored model-facing short IDs, then cleared unconditionally on every
snipCompactIfNeeded pass. With concurrent in-process sessions, session B could
clear A's pending IDs (losing A's snip) or, on a short-ID collision, prune the
wrong message. Resolve short IDs to full UUIDs at mark time against the
snipping conversation's own messages, and consume only the UUIDs present in
the current message array. UUIDs are globally unique, so the registry
self-scopes: one session can no longer consume or mis-target another's.
* fix(history-snip): drop paired assistant tool-use when snipping a tool-result message
[id:] tags are appended to user messages only, so the model snips a
tool-result user message, not the assistant tool_use. The previous
pairing only ran assistant->user; snipping a tool-result left the
preceding assistant tool_use orphaned, so the next API-prep pass
synthesized a placeholder result and the tool interaction was never
actually removed from live context or from replay.
Pair in both directions: when a snipped user message's tool_results all
belong to an assistant turn, drop that assistant tool_use too (mirroring
the existing .every() guard so partially-snipped turns are kept), and
record its UUID in the boundary's removedUuids so replay drops the same
set.
* fix(history-snip): add SnipBoundaryMessage render component
HISTORY_SNIP ships enabled, so Message.tsx reaches the snip_boundary
render branch after the first snip. That branch requires
./messages/SnipBoundaryMessage.js and renders its named
SnipBoundaryMessage export, but no source file existed — the build
emitted a missing-module-stub exporting only a default noop, so the
named component was undefined and the render crashed right after a
successful snip.
Add the component, mirroring CompactBoundaryMessage: a single dimmed
line marking the snip with the removed-message count and the transcript
shortcut. The build now resolves the import (no stub) and the named
export is present in the bundle.
* build: guard against enabled-feature imports resolving to missing-module stubs
The missing-import scanner stubs any unresolved relative import to a noop
default export. For a require behind a DISABLED feature flag that is correct
(dead-code-eliminated, never bundled). But when a flag is ENABLED the gated
require becomes live and the stub silently degrades a real module to
() => null, so a named export resolves to undefined and crashes the first
time that path runs. SnipBoundaryMessage shipped exactly this way: build,
smoke, and unit tests all passed while the UI crashed on the first snip.
Feature-flag DCE removes disabled branches before bundling, so every
missing-module-stub marker left in dist/cli.mjs is reachable in the shipped
build. After the CLI bundle, fail the build on any stub marker not explicitly
grandfathered in ACCEPTABLE_RUNTIME_STUBS (seeded with the pre-existing
stubs), and warn on stale allowlist entries. Verified: removing the
SnipBoundaryMessage source makes the guard fail and name the module.
* fix(history-snip): drop unmirrored force-snip command registration
force-snip is gated on HISTORY_SNIP but its source (./commands/force-snip.js)
was never mirrored into this build, so require(...).default resolved to the
missing-module stub's noop. That truthy noop was spread into the command list
(commands.ts:252), registering a bare () => null as a command with no name,
description, or call — broken the moment anything enumerates commands. Enabling
HISTORY_SNIP turned this live, same class as the SnipBoundaryMessage crash.
Remove the registration rather than ship a phantom command: the implementation
is not present in this tree, so the honest behavior is to not register it. Drop
the matching ACCEPTABLE_RUNTIME_STUBS entry so the bundle guard stays strict.
* fix(history-snip): don't snip a tool result that would orphan a surviving tool_use
The result-side pairing dropped an explicitly-snipped tool-result user
message even when its paired assistant turn had other, un-snipped tool
calls. That left the assistant holding a tool_use with no matching result,
which the next API-prep pass repairs with a synthetic placeholder
(src/utils/messages.ts), so the snip never actually took effect and the
restored context still carried the stale interaction.
Block-level surgery on the surviving assistant is not an option: replay
(projectSnippedView / loadTranscriptFile) drops whole UUIDs, not blocks, so
the live store and a --resume would diverge. Instead, treat an unclean snip
as a no-op: a tool_use is safely removable only if its whole assistant turn
goes with it (the assistant is explicitly snipped, or every tool_use in it
has its result snipped). A tool-result user message whose results don't all
pair to a removable tool_use is kept, and no boundary is emitted when nothing
was cleanly removable, keeping live context and replay identical.
* docs(build): document the bundle-stub guard as a coarse tripwire
The guard rationale claimed every missing-module-stub marker left in
dist/cli.mjs is reachable in the shipped build and that each allowlisted
entry is latent runtime debt behind an enabled flag. That overstates it: the
scanner keys missing modules by specifier string, so a same-named specifier
missing in one importer (including a test file) can leave a marker even when
another importer resolves the real module, and a marker can sit on a path
that never runs. Reword the comment and error message so a flagged stub reads
as "inspect this", not "confirmed runtime crash"; the guard reliably catches
a NEW stub appearing where none was expected, which is its actual value.
* fix(build): canonicalize bundle stub markers before diffing the allowlist
The bundle guard compared raw `missing-module-stub:` marker text against
ACCEPTABLE_RUNTIME_STUBS, but the marker format is not stable across build
hosts: locally Bun emits the relative import specifier
(`./commands/fork/index.js`), while on the Linux CI merge run it emitted the
same grandfathered stubs as absolute source paths
(`/home/runner/work/openclaude/openclaude/src/commands/fork/index.ts`). The raw
diff therefore failed `bun run smoke` on CI for already-allowlisted stubs and
also reported them as stale.
Canonicalize both the bundle markers and the allowlist to a stable key (the
basename without extension) before diffing, so a stub matches in either form.
Basename is the only reduction that unifies a relative specifier of unknown
depth with an absolute path (a fixed path-segment count breaks single-segment
specifiers like `./dream.js`). The allowlist keeps the readable full specifiers;
diagnostics still print the raw marker. Guard against two allowlist entries
sharing a basename (which would let one silently cover an unrelated stub) by
failing the build if the canonical set is smaller than the allowlist.
* chore(build): drop allowlist stubs resolved by current main
Rebasing onto current main brings in the per-importer scanner (#1399)
and the real sources for four previously-stubbed modules, so they no
longer emit missing-module markers:
- ../../utils/hooks/ssrfGuard.js (per-importer keying, #1399/#1450)
- ./dream.js (/dream restored, #1399)
- ./UserForkBoilerplateMessage.js (source mirrored, #1451)
- ./commands/fork/index.js (unmirrored /fork dropped, #1451)
The bundle guard flagged all four as stale allowlist entries. Remove
them and refresh the guard rationale comment, which described the
pre-#1399 specifier-string scanner; the scanner now keys per importer.
* fix(history-snip): expose snip id on pure tool-result messages
appendMessageTagToUserMessage() only appended the [id:...] tag to a
string body or an existing text block. A user message that is purely
tool_result blocks (the normal shape for large Read/Bash outputs) has
no text block, so it returned unchanged and carried no visible id. Those
are exactly the highest-value snip targets the feature prompts the model
to remove, yet the model had no id to reference them by.
Append a dedicated text block holding the tag when a tool-result-only
message has no text block. The tool_result block is left intact, so snip
pairing is unaffected, and the tag lands on the API-bound copy only.
Export the function and add colocated tests covering string body, text
block, the pure tool_result case, and meta passthrough.
* fix(build): key bundle-stub guard on repo-relative path, not basename
The guard canonicalized every missing-module-stub marker to its basename
before checking the allowlist, so a future stub named constants.ts (or
cachedMCConfig.ts, MonitorMcpDetailDialog.ts) from any other directory
would be treated as allowlisted and slip past the guard — the exact
regression class the guard exists to catch.
Post-#1399 the per-importer scanner records each stub as the resolved
absolute source path, which differs across build hosts only by the
repo-root prefix. So key on the repo-relative path from src/ onward
(without extension): stable across hosts yet path-specific, so a stub
cannot mask a same-named file elsewhere. Drop the now-moot basename
collision guard and store the allowlist as repo-relative keys.
* fix(history-snip): describe snip as a queued, refusable request
SnipTool's tool result said "Marked N message(s) for removal. They will
be removed from context before the next model call" based only on the
count of input IDs. But snipCompactIfNeeded() can refuse the exact
request on the next turn: it keeps a tool_result whose paired tool_use
would survive (snipping it would orphan the tool call), freeing 0 tokens
and emitting no boundary. The model was told the output would be removed,
then saw it still in context with no failure signal, so it treated a
structural no-op as a successful context reduction.
Reword the tool result to describe the snip as a queued request that may
be refused, name the one refusal condition (would orphan a paired tool
call, e.g. one result from a parallel-tool turn), and give the model the
observable signal and repair: a kept message re-shows its [id:...] tag
next turn (tags are re-applied every API-prep pass), and snipping all of
that turn's tool results together removes them cleanly.
Add SnipTool.test.ts pinning the queued/refusable wording.
* test(history-snip): import UserMessage from its canonical module
messages.snipTag.test.ts imported UserMessage from ../query.js, which
imports the type but does not re-export it (TS2459). Import it from
../types/message.js, the canonical source messages.ts itself uses, so
the snip test files typecheck cleanly.
* fix(history-snip): make snip id tag injection idempotent
appendMessageTagToUserMessage() documents that it only mutates the
API-bound copy, but query.ts builds the next loop state's toolResults
from normalizeMessagesForAPI([update.message]) (query.ts:1589) and stores
that normalized, already-tagged output into state.messages
(query.ts:1976). With HISTORY_SNIP enabled the tag is carried forward as
conversation state, so the next turn re-normalizes it and appends the
same [id:...] a second time. In multi-tool agent loops every prior tool
result accumulates another duplicate tag each iteration, bloating context
and showing the model repeated IDs that are meant to be an API-projection
affordance only.
Guard the append: if the message already carries its own [id:<id>] token
(string body, last text block, or the dedicated tool_result text block),
return it unchanged. The token is derived from the message's own uuid, so
its presence means it was already tagged. Adds 3 idempotency tests.
* fix(history-snip): expose every parallel-tool sibling id before merge
normalizeMessagesForAPI tagged snip [id:] markers only after merging
consecutive user messages. A parallel-tool assistant turn yields several
adjacent tool_result user messages; the merge keeps just the first
operand's uuid, so on the resume/reload path (where the persisted
transcript is the untagged original) only the first sibling's id reached
the model. snipCompactIfNeeded refuses to drop one result of such a turn
(it would orphan the surviving tool_use), so the model needed every
sibling's id to request the whole-turn removal the snip prompt instructs,
and could never form it: a permanent no-op.
Inject the tag per user message before the merge instead, so each
sibling carries its own [id:] and joinTextAtSeam preserves them all,
matching the live path where each result is tagged at push time. The
post-merge sweep stays (idempotent) to tag user messages synthesized
during normalization (local_command, attachments).
Test: merging tagged parallel siblings keeps every sibling id and both
tool_result blocks.
* test(history-snip): type snip-replay test ids as UUID
loadTranscriptFile() returns Map<UUID, TranscriptMessage>, but the test
id() helper returned plain string, so every messages.has/get/
buildConversationChain call in the persisted-snip replay test raised a
TS2345 against the UUID-keyed map. Type id() as UUID (casting the literal
once at the source) so the new replay coverage does not add touched-path
typecheck debt. Also clears the same error cluster in the pre-existing
compact-boundary tests that share the helper.
* docs(history-snip): drop removed /force-snip from setMessages comment
The QueryEngine setMessages comment cited /force-snip as its example of a
message-mutating slash command, but that command was removed. Point the
example at /clear (src/commands/clear/conversation.ts), which still mutates
the message array via setMessages, so the comment stays accurate.
* refactor(history-snip): type SnipBoundaryMessage removedUuids as string[]
removedUuids holds message UUID strings throughout the snip feature, but
the SnipBoundaryMessage prop typed it as unknown[]. Narrow it to string[]
so the type carries intent and the test fixture no longer needs an
`as never` cast to satisfy the prop (the cast bypassed type checking and
could have hidden a real fixture/prop mismatch).
* fix(history-snip): drop stale cachedMCConfig stub-allowlist entry
cachedMCConfig.ts now exists in the tree and bundles as real code, so it
is no longer emitted as a missing-module stub. The grandfathered baseline
listed it among acceptable stubs, which made the new guard print a stale
warning and, worse, would silently accept a future reintroduced
cachedMCConfig stub as known debt instead of flagging it. Drop the entry so
the allowlist matches the actual bundle (VerifyPlanExecutionTool/constants
and MonitorMcpDetailDialog).
* fix(history-snip): guard paired snip drops and report queued count
Two CodeRabbit findings on the snip compaction path:
- Mixed-content turns: the inferred paired-drop ran its .every() check over
filtered tool blocks only, so an assistant turn like [text, tool_use] (or a
user [tool_result, text]) was treated as fully droppable and its text was
silently removed when the paired half was snipped. Require the whole message
to be tool blocks before an inferred drop; otherwise treat the snip as a
no-op (the explicit-snip path, where the model deliberately targets a message,
is unchanged and still removes wholesale).
- Queued count: markForSnip only enqueues short IDs it can resolve against the
conversation, but SnipTool reported sniped = input.message_ids.length, which
overstated the result when IDs were stale or unresolvable. markForSnip now
returns the distinct resolved UUIDs and SnipTool reports that length.
* fix(history-snip): align snip prompt with queued-not-guaranteed contract
The tool description told the model snipped IDs are "permanently remove[d]
... before the next model call", but snipCompactIfNeeded queues the request
and keeps a message when removing it would orphan a paired tool_use (the
tool_result already says so). Match the description to that contract so the
model does not treat a structural no-op as a guaranteed removal.
OpenClaude
OpenClaude is an open-source coding-agent CLI for cloud and local model providers.
Use OpenAI-compatible APIs, Gemini, GitHub Models, Codex OAuth, Codex, Ollama, Atomic Chat, and other supported backends while keeping one terminal-first workflow: prompts, tools, agents, MCP, slash commands, and streaming output.
OpenClaude is also mirrored to GitLawb: gitlawb.com/node/repos/z6MkqDnb/openclaude
Quick Start | Setup Guides | Providers | Source Build | VS Code Extension | Sponsors | Community
Sponsors
|
|
|
|
|
| GitLawb | Bankr.bot | Atomic Chat | Xiaomi MiMo | Atlas Cloud |
Star History
Why OpenClaude
- Use one CLI across cloud APIs and local model backends
- Save provider profiles inside the app with
/provider - Run with OpenAI-compatible services, Gemini, GitHub Models, Codex OAuth, Codex, Ollama, Atomic Chat, and other supported providers
- Keep coding-agent workflows in one place: bash, file tools, grep, glob, agents, tasks, MCP, and web tools
- Use the bundled VS Code extension for launch integration and theme support
Quick Start
Install
npm install -g @gitlawb/openclaude@latest
If you're on Arch Linux, you can install OpenClaude from the community-maintained AUR package:
paru -S openclaude
If the install later reports ripgrep not found, install ripgrep system-wide and confirm rg --version works in the same terminal before starting OpenClaude.
Verify / troubleshoot installed version:
openclaude --version
npm view @gitlawb/openclaude dist-tags
npm install -g @gitlawb/openclaude@latest
Start
openclaude
Inside OpenClaude:
- run
/providerfor guided provider setup and saved profiles - run
/onboard-githubfor GitHub Models onboarding
Fastest OpenAI setup
macOS / Linux:
export CLAUDE_CODE_USE_OPENAI=1
export OPENAI_API_KEY=sk-your-key-here
export OPENAI_MODEL=gpt-4o
openclaude
Windows PowerShell:
$env:CLAUDE_CODE_USE_OPENAI="1"
$env:OPENAI_API_KEY="sk-your-key-here"
$env:OPENAI_MODEL="gpt-4o"
openclaude
Fastest local Ollama setup
macOS / Linux:
export CLAUDE_CODE_USE_OPENAI=1
export OPENAI_BASE_URL=http://localhost:11434/v1
export OPENAI_MODEL=qwen2.5-coder:7b
openclaude
Windows PowerShell:
$env:CLAUDE_CODE_USE_OPENAI="1"
$env:OPENAI_BASE_URL="http://localhost:11434/v1"
$env:OPENAI_MODEL="qwen2.5-coder:7b"
openclaude
Setup Guides
Beginner-friendly guides:
Advanced and source-build guides:
Supported Providers
| Provider | Setup Path | Notes |
|---|---|---|
| OpenAI-compatible | /provider or env vars |
Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible /v1 servers |
| Hicap | /provider or OpenAI-compatible env vars |
Uses api-key auth, discovers models from unauthenticated /models, and supports Responses mode for gpt- models |
| Gemini | /provider or env vars |
Supports API key only |
| GitHub Models | /onboard-github |
Interactive onboarding with saved credentials |
| Codex OAuth | /provider |
Opens ChatGPT sign-in in your browser and stores Codex credentials securely |
| Codex | /provider |
Uses existing Codex CLI auth, OpenClaude secure storage, or env credentials |
| Gitlawb Opengateway | Startup default, /provider, or env vars |
Smart gateway at https://opengateway.gitlawb.com/v1; requires an API key from https://gitlawb.com/opengateway/keys and routes Xiaomi MiMo and GMI Cloud partner models by OPENAI_MODEL |
| OpenCode Zen | /provider or env vars |
Pay-as-you-go AI gateway (41 models); uses OPENCODE_API_KEY via https://opencode.ai/zen/v1; shared key with OpenCode Go |
| OpenCode Go | /provider or env vars |
$10/mo subscription for open models (12 models); uses OPENCODE_API_KEY via https://opencode.ai/zen/go/v1; shared key with OpenCode Zen |
| Xiaomi MiMo | /provider or env vars |
OpenAI-compatible API at https://mimo.mi.com; uses MIMO_API_KEY and defaults to mimo-v2.5-pro |
| Ollama | /provider or env vars |
Local inference with no API key |
| Atomic Chat | /provider, env vars, or bun run dev:atomic-chat |
Local Model Provider; auto-detects loaded models |
| Bedrock / Vertex / Foundry | env vars | Anthropic-family cloud routes; Vertex is for Claude on Vertex AI, not arbitrary Model Garden models |
What Works
- Tool-driven coding workflows: Bash, file read/write/edit, grep, glob, agents, tasks, MCP, and slash commands
- Streaming responses: Real-time token output and tool progress
- Tool calling: Multi-step tool loops with model calls, tool execution, and follow-up responses
- Images: URL and base64 image inputs for providers that support vision
- Provider profiles: Guided setup plus saved user-level provider profile support
- Local and remote model backends: Cloud APIs, local servers, and Apple Silicon local inference
Provider Notes
OpenClaude supports multiple providers, but behavior is not identical across all of them.
- Anthropic-specific features may not exist on other providers
- Tool quality depends heavily on the selected model
- Smaller local models can struggle with long multi-step tool flows
- Some providers impose lower output caps than the CLI defaults, and OpenClaude adapts where possible
- Gitlawb Opengateway is the fresh-install startup default and requires an API key from https://gitlawb.com/opengateway/keys. It uses one OpenAI-compatible base URL; switch between
mimo-*andgoogle/gemini-3.1-flash-lite-previewwith/model, and do not pin the base URL to/v1/xiaomi-mimo. - Xiaomi MiMo uses
api-keyheader auth on the direct OpenAI-compatible route and currently does not support/usagereporting in OpenClaude
For best results, use models with strong tool/function calling support.
Agent Routing
OpenClaude can route different agents to different models through settings-based routing. This is useful for cost optimization or splitting work by model strength.
Add to ~/.openclaude.json:
{
"agentModels": {
"deepseek-v4-flash": {
"base_url": "https://api.deepseek.com/v1",
"api_key": "sk-your-key"
},
"zai-default": {
"model": "glm-5.1",
"base_url": "https://api.z.ai/api/coding/paas/v4",
"api_key": "sk-your-key"
},
"gpt-4o": {
"base_url": "https://api.openai.com/v1",
"api_key": "sk-your-key"
}
},
"agentRouting": {
"Explore": "deepseek-v4-flash",
"Plan": "gpt-4o",
"general-purpose": "gpt-4o",
"frontend-dev": "zai-default",
"default": "gpt-4o"
}
}
When no routing match is found, the global provider remains the fallback.
agentRouting values and explicit Agent tool model overrides match keys in agentModels. By default, that key is also the model string sent to the provider. Set agentModels.<key>.model when you want a local route key such as zai-default to call a different provider model name such as glm-5.1.
Note:
/providerchanges the global/parent provider for your current session.agentModelsandagentRoutingare specifically for configuring per-agent provider overrides while keeping the parent session unchanged.
Note:
api_keyvalues insettings.jsonare stored in plaintext. Keep this file private and do not commit it to version control.
Web Search and Fetch
By default, WebSearch works on non-Anthropic models using DuckDuckGo. This gives GPT-4o, DeepSeek, Gemini, Ollama, and other OpenAI-compatible providers a free web search path out of the box.
Note: DuckDuckGo fallback works by scraping search results and may be rate-limited, blocked, or subject to DuckDuckGo's Terms of Service. If you want a more reliable supported option, configure Firecrawl.
For Anthropic-native backends and Codex responses, OpenClaude keeps the native provider web search behavior.
WebFetch works, but its basic HTTP plus HTML-to-markdown path can still fail on JavaScript-rendered sites or sites that block plain HTTP requests.
Set a Firecrawl API key if you want Firecrawl-powered search/fetch behavior:
export FIRECRAWL_API_KEY=your-key-here
With Firecrawl enabled:
WebSearchcan use Firecrawl's search API while DuckDuckGo remains the default free path for non-Claude modelsWebFetchuses Firecrawl's scrape endpoint instead of raw HTTP, handling JS-rendered pages correctly
Free tier at firecrawl.dev includes 500 credits. The key is optional.
Headless gRPC Server
OpenClaude can be run as a headless gRPC service, allowing you to integrate its agentic capabilities (tools, bash, file editing) into other applications, CI/CD pipelines, or custom user interfaces. The server uses bidirectional streaming to send real-time text chunks, tool calls, and request permissions for sensitive commands.
1. Start the gRPC Server
Start the core engine as a gRPC service on localhost:50051:
npm run dev:grpc
Configuration
| Variable | Default | Description |
|---|---|---|
GRPC_PORT |
50051 |
Port the gRPC server listens on |
GRPC_HOST |
localhost |
Bind address. Use 0.0.0.0 to expose on all interfaces (not recommended without authentication) |
2. Run the Test CLI Client
We provide a lightweight CLI client that communicates exclusively over gRPC. It acts just like the main interactive CLI, rendering colors, streaming tokens, and prompting you for tool permissions (y/n) via the gRPC action_required event.
In a separate terminal, run:
npm run dev:grpc:cli
Note: The gRPC definitions are located in src/proto/openclaude.proto. You can use this file to generate clients in Python, Go, Rust, or any other language.
Source Build And Local Development
bun install
bun run build
node dist/cli.mjs
Helpful commands:
bun run devbun testbun run test:coveragebun run security:pr-scan -- --base origin/mainbun run smokebun run doctor:runtimebun run verify:privacy- focused
bun test ...runs for the areas you touch
Testing And Coverage
OpenClaude uses Bun's built-in test runner for unit tests.
Run the full unit suite:
bun test
Generate unit test coverage:
bun run test:coverage
Open the visual coverage report:
open coverage/index.html
If you already have coverage/lcov.info and only want to rebuild the UI:
bun run test:coverage:ui
Use focused test runs when you only touch one area:
bun run test:providerbun run test:provider-recommendationbun test path/to/file.test.ts
Recommended contributor validation before opening a PR:
bun run buildbun run smokebun run test:coveragefor broader unit coverage when your change affects shared runtime or provider logic- focused
bun test ...runs for the files and flows you changed
Coverage output is written to coverage/lcov.info, and OpenClaude also generates a git-activity-style heatmap at coverage/index.html.
Repository Structure
src/- core CLI/runtimescripts/- build, verification, and maintenance scriptsdocs/- setup, contributor, and project documentationpython/- standalone Python helpers and their testsvscode-extension/openclaude-vscode/- VS Code extension.github/- repo automation, templates, and CI configurationbin/- CLI launcher entrypoints
VS Code Extension
The repo includes a VS Code extension in vscode-extension/openclaude-vscode for OpenClaude launch integration, provider-aware control-center UI, and theme support.
Security
If you believe you found a security issue, see SECURITY.md.
Community
- Use GitHub Discussions for Q&A, ideas, and community conversation
- Use GitHub Issues for confirmed bugs and actionable feature work
Contributing
Contributions are welcome.
For larger changes, open an issue first so the scope is clear before implementation. Helpful validation commands include:
bun run buildbun run test:coveragebun run smoke- focused
bun test ...runs for files and flows you changed
Disclaimer
OpenClaude is an independent community project and is not affiliated with, endorsed by, or sponsored by Anthropic.
OpenClaude originated from the Claude Code codebase and has since been substantially modified to support multiple providers and open use. "Claude" and "Claude Code" are trademarks of Anthropic PBC. See LICENSE for details.
License
See LICENSE.