
Security News
White House Authorizes Private Companies to Conduct Offensive Cyber Operations
A new federal program will let vetted U.S. cybersecurity firms help investigate and disrupt foreign cybercrime groups under government direction.
A React + Hono toolkit that puts a browser UI on the Claude Agent SDK — the same agentic framework that powers Claude Code. Streams tool calls, file edits, permissions, and multi-turn sessions over SSE into ready-made React components.
The Claude Agent SDK gives you a powerful agentic loop — but it's a server-side AsyncGenerator with no opinion on how to get those events to a browser. neeter bridges that gap:
PushChannel + SessionManager let users send messages at any time. Messages queue and the SDK picks them up when ready — no "wait for the agent to finish" lockout.MessageTranslator reshapes them into semantically named SSE events (text_delta, tool_start, tool_call, tool_result, ...) that the browser's EventSource can route with native addEventListener.pending → streaming_input → running → complete phases with streaming JSON input, giving your UI fine-grained control over loading states and progressive rendering.onToolResult and emit typed { name, value } events for app-specific reactivity (e.g. "document saved", "data refreshed") without touching the core protocol.canUseTool callback fires on the server, but your users are in the browser. PermissionGate bridges the gap with deferred promises, SSE events, and an HTTP POST endpoint — the agent blocks until the user clicks Allow/Deny or answers a clarifying question.pnpm add neeter
Peer dependencies:
{
"@anthropic-ai/claude-agent-sdk": ">=0.2.0",
"hono": ">=4.0.0",
"react": ">=18.0.0",
"react-markdown": ">=10.0.0",
"zustand": ">=5.0.0",
"immer": ">=10.0.0",
"tailwindcss": ">=4.0.0"
}
neeter/server gives you a Hono router that manages Agent SDK sessions and streams events to the client over SSE.
The Claude Agent SDK reads your API key from the environment automatically. Make sure it's set before starting your server:
export ANTHROPIC_API_KEY=your-api-key
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import {
createAgentRouter,
SessionManager,
MessageTranslator,
} from "neeter/server";
const sessions = new SessionManager(() => ({
context: {},
model: "claude-sonnet-4-5-20250929",
systemPrompt: "You are a helpful assistant.",
maxTurns: 50,
}));
const translator = new MessageTranslator();
const app = new Hono();
app.route("/", createAgentRouter({ sessions, translator }));
serve({ fetch: app.fetch, port: 3000 });
Enable Claude's chain-of-thought reasoning by setting thinking in your session config:
const sessions = new SessionManager(() => ({
context: {},
model: "claude-sonnet-4-5-20250929",
systemPrompt: "You are a helpful assistant.",
thinking: { type: "enabled", budgetTokens: 10000 },
}));
When enabled, thinking blocks stream to the client as thinking_delta SSE events and render as collapsible cards in MessageList. Set { type: "disabled" } to explicitly turn thinking off (it's off by default).
This gives you four endpoints:
| Method | Path | Description |
|---|---|---|
POST | /api/sessions | Create a session, returns { sessionId } |
POST | /api/sessions/:id/messages | Send { text } to a session |
GET | /api/sessions/:id/events | SSE stream of agent events |
POST | /api/sessions/:id/permissions | Respond to a permission request (see Permissions) |
SessionManager takes a factory function that runs once per session. The generic type parameter lets you attach per-session state:
interface MyContext {
history: string[];
}
const sessions = new SessionManager<MyContext>(() => ({
context: { history: [] },
model: "claude-sonnet-4-5-20250929",
systemPrompt: "You are a helpful assistant.",
mcpServers: { myServer: createMyServer() },
allowedTools: ["mcp__myServer__*"],
maxTurns: 100,
}));
The context is available in translator hooks (see below).
Use onToolResult to inspect what the agent did and emit structured custom events:
const translator = new MessageTranslator<MyContext>({
onToolResult: (toolName, result, session) => {
if (toolName === "save_note") {
session.context.history.push(result);
return [{ name: "notes_updated", value: session.context.history }];
}
return [];
},
});
Each returned { name, value } object is sent to the client as a custom SSE event.
By default sessions run with permissionMode: "bypassPermissions" — all tools execute automatically. Set permissionMode to "default" (or "acceptEdits") to require browser-side approval before each tool runs:
const sessions = new SessionManager(() => ({
context: {},
model: "claude-sonnet-4-5-20250929",
systemPrompt: "You are a helpful assistant.",
permissionMode: "default",
}));
When permissionMode is not "bypassPermissions":
AskUserQuestion calls surface as structured questions with optionspermission_request SSE events fire to the client/api/sessions/:id/permissionsThe PermissionGate on each session manages the deferred promises internally — no additional wiring needed.
| Mode | Behavior |
|---|---|
"bypassPermissions" | All tools auto-approved (default) |
"default" | Every tool call requires explicit approval |
"acceptEdits" | File edits auto-approved, other tools require approval |
"plan" | Planning mode — SDK-defined behavior |
neeter/react provides a drop-in chat UI that connects to your server.
import { AgentProvider, MessageList, ChatInput, useAgentContext } from "neeter/react";
function App() {
return (
<AgentProvider>
<Chat />
</AgentProvider>
);
}
function Chat() {
const { sendMessage } = useAgentContext();
return (
<div className="flex h-screen flex-col">
<MessageList className="flex-1" />
<ChatInput onSend={sendMessage} />
</div>
);
}
Components use Tailwind utility classes and accept className for overrides.
If your server emits custom events (via onToolResult), handle them with onCustomEvent:
<AgentProvider
onCustomEvent={(e) => {
if (e.name === "notes_updated") {
myStore.getState().setNotes(e.value);
}
}}
>
<Chat />
</AgentProvider>
Each event is a typed CustomEvent<T> with name and value fields.
When you add tools to your SessionManager, neeter automatically renders them with purpose-built widgets — diff views for edits, code blocks for file reads, expandable link pills for web searches, and so on. No configuration needed.
Tool calls without a registered widget fall back to a minimal status indicator.
Each tool call moves through phases, reflected in WidgetProps.phase:
| Phase | Trigger | What's available |
|---|---|---|
pending | tool_start SSE event | input: {} |
streaming_input | tool_input_delta events | partialInput accumulates |
running | tool_call event (input finalized) | input is complete |
complete | tool_result event | result is JSON-parsed |
error | Error during execution | error message |
Neeter components use Tailwind v4 utility classes and shadcn/ui-compatible CSS variable names (bg-primary, text-muted-foreground, border-border, etc.).
Your existing theme variables are already compatible. Add one line to your main CSS so Tailwind scans neeter's component source for utility classes:
@import "tailwindcss";
@source "../node_modules/neeter/src";
The @source path is relative to your CSS file — adjust if your stylesheet lives in a nested directory (e.g. ../../node_modules/neeter/src).
Import the bundled theme, which includes source scanning automatically:
@import "tailwindcss";
@import "neeter/theme.css";
This provides a neutral OKLCH palette with light + dark mode support and the Tailwind v4 @theme inline variable bridge.
Dark mode activates via:
.dark class on <html> (recommended), orprefers-color-scheme: dark system preference (automatic)Add .light to <html> to force light mode when using system preference detection.
Drop the neeter/theme.css import and add @source — your shadcn theme takes over with zero migration.
neeter/server| Export | Description |
|---|---|
SessionManager<TCtx> | Manages agent sessions with per-session context |
Session<TCtx> | A single session — id, context, pushMessage(), permissionGate, abort() |
SessionInit<TCtx> | Factory return type — model, systemPrompt, permissionMode, mcpServers, etc. |
MessageTranslator<TCtx> | Converts SDK messages to SSE events |
TranslatorConfig<TCtx> | Translator options — onToolResult hook |
createAgentRouter<TCtx>(config) | Returns a Hono app with session, SSE, and permission routes |
PermissionGate | Per-session deferred-promise map for tool approval and user questions |
PushChannel<T> | Async iterable queue for feeding messages to the SDK |
sseEncode(event) | Formats an SSEEvent as an SSE string |
streamSession(session, translator) | Async generator yielding SSEEvents |
neeter/react| Export | Description |
|---|---|
AgentProvider | Context provider — wraps store + SSE connection |
useAgentContext() | Returns { sessionId, sendMessage, respondToPermission, store } |
useChatStore(selector) | Zustand selector hook into chat state |
createChatStore() | Creates a vanilla Zustand store (for advanced use) |
useAgent(store, config?) | SSE connection hook (used internally by AgentProvider) |
MessageList | Auto-scrolling message list with pending permissions and thinking indicator |
TextMessage | Markdown-rendered message bubble |
ChatInput | Textarea + send button |
ToolCallCard | Lifecycle-aware tool call display with inline approval |
PendingPermissions | Renders pending tool approval and user question cards |
ToolApprovalCard | Tool approval card with Allow/Deny buttons |
UserQuestionCard | Structured question card with option selection |
ThinkingBlock | Collapsible card displaying extended thinking text |
ThinkingIndicator | Animated dots shown while agent is generating |
CollapsibleCard | Expandable card wrapper |
StatusDot | Phase-colored status indicator |
cn(...inputs) | clsx + tailwind-merge utility for class merging |
registerWidget(registration) | Register a component for a tool name |
getWidget(toolName) | Look up a registered widget |
stripMcpPrefix(name) | "mcp__server__tool" → "tool" |
| Type | Description |
|---|---|
SSEEvent | { event: string, data: string } |
ChatMessage | { id, role, content, thinking?, toolCalls? } |
ToolCallInfo | { id, name, input, partialInput?, result?, error?, status } |
ToolCallPhase | "pending" | "streaming_input" | "running" | "complete" | "error" |
WidgetProps<TResult> | Props passed to widget components |
WidgetRegistration<TResult> | Widget registration — toolName, label, richLabel?, inputRenderer?, component |
ChatStore | StoreApi<ChatStoreShape> — vanilla Zustand store |
ChatStoreShape | Full state + actions interface |
CustomEvent<T> | { name: string, value: T } — structured app-level event |
PermissionRequest | ToolApprovalRequest | UserQuestionRequest — pending permission |
PermissionResponse | ToolApprovalResponse | UserQuestionResponse — user's answer |
ToolApprovalRequest | { kind, requestId, toolName, toolUseId?, input, description? } |
ToolApprovalResponse | { kind, requestId, behavior: "allow" | "deny", message? } |
UserQuestion | { question, header?, options?, multiSelect? } |
UserQuestionRequest | { kind, requestId, questions: UserQuestion[] } |
UserQuestionResponse | { kind, requestId, answers: Record<string, string> } |
Events emitted by the server, handled automatically by useAgent:
| Event | Payload | Description |
|---|---|---|
message_start | {} | Agent began generating a response |
thinking_start | {} | Extended thinking block began |
thinking_delta | { text } | Streaming thinking text chunk |
text_delta | { text } | Streaming text chunk |
tool_start | { id, name } | Agent began calling a tool |
tool_input_delta | { id, partialJson } | Streaming tool input JSON |
tool_call | { id, name, input } | Tool input finalized |
tool_result | { toolUseId, result } | Tool execution result |
tool_progress | { toolName, elapsed } | Long-running tool heartbeat |
permission_request | PermissionRequest | Tool approval or user question awaiting response |
turn_complete | { numTurns, cost } | Agent turn finished |
custom | { name, value } | App-specific event from onToolResult |
session_error | { subtype } | Session ended with error |
See docs/development.md for local setup, pre-commit hooks, and CI.
MIT
FAQs
React + Hono toolkit for building chat UIs on top of the Claude Agent SDK
The npm package neeter receives a total of 0 weekly downloads. As such, neeter popularity was classified as not popular.
We found that neeter demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
A new federal program will let vetted U.S. cybersecurity firms help investigate and disrupt foreign cybercrime groups under government direction.

Research
/Security News
The campaign amassed more than 75,000 installs by targeting Russian-speaking users seeking access to blocked services.

Company News
Open source maintainers are under more pressure than ever. We're raising our open source program from the Team plan to the Business plan, free.