picoclaw
CLAUDE.md1 starsChanged 8 months agoMITView on GitHub
Serverless-first Claude Agent runtime. HTTP API for conversations and scheduled tasks. Forked from NanoClaw — no Docker child containers, no channel adapters. Full tooling reference: make help shows all Makefile targets. The Dockerfile uses a multi-stage build — TypeScript compilation happens inside Docker, so no local Node.js is required for building images. Alternatively: make docker-build && make docker-run for manual control. Images are published to ghcr.io/breakcafe/picoclaw. On main, tags are latest, <version>, <version>-<commit>. On other branches, tags are dev, dev-<commit>,…
- Reads credentials
In this repository20 other files it gives its agents.
- add-discordSkill
- add-gmailSkill
- add-image-visionSkill
- add-ollama-toolSkill
- add-parallelSkill
- add-pdf-readerSkill
- add-slackSkill
- add-telegramSkill
- add-telegram-swarmSkill
- add-voice-transcriptionSkill
- add-whatsappSkill
- convert-to-apple-containerSkill
- customizeSkill
- debugSkill
- get-qodo-rulesSkill
- qodo-pr-resolverSkill
- setupSkill
- update-nanoclawSkill
- use-local-whisperSkill
- x-integrationSkill
The file
Show its rules and text
We split this CLAUDE.md into the rules it gives an agent
13 rules. About 0 are common, 3 are distinctive, and 10 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 167
When ORG_DIR is empty or $ORG_DIR/CLAUDE.md does not exist, systemPrompt is undefined and the SDK falls back to the default Claude Code …
- Very specific: line 169
When SYSTEM_PROMPT_OVERRIDE is set, it takes precedence over both the preset and org CLAUDE.md.
- Very specific: line 177
When ORG_DIR is set and $ORG_DIR/managed-mcp.json exists, src/managed-mcp.ts reads it at startup and caches the server configs in memory.
- Very specific: line 192
Create src/routes/myroute.ts with export function myRoutes(): Router
- Very specific: line 194
Add test in src/routes/myroute.test.ts using supertest (see server.test.ts for pattern)
- Very specific: line 198
Add tool definition in src/mcp-server.ts under the server.tool() section
- Very specific: line 199
Use z (zod) for input validation
- Very specific: line 200
Read/write via the shared SQLite db instance
- Distinctive: line 347
Never write directly to the volume path.
- Very specific: line 355
Isolated task conversations: When context_mode=isolated, the MCP server calls ensureConversationExists() before creating a task to avoid FK …
- Very specific: line 358
Use format:check to verify without modifying.
- Distinctive: line 383
After any code change, verify:
- Distinctive: line 394
Stop path still syncs data and exits cleanly
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
PicoClaw
Serverless-first Claude Agent runtime. HTTP API for conversations and scheduled tasks. Forked from NanoClaw — no Docker child containers, no channel adapters.
Code Style
- ESM modules with explicit
.jsextensions in imports:import { foo } from './bar.js' - Single quotes, 2-space indentation (Prettier enforced, see
.prettierrc) - Named exports only, no default exports
- TypeScript strict mode; interfaces for data types (not type aliases)
- File naming:
kebab-case.ts, tests colocated askebab-case.test.ts - Logger:
logger.info({ context }, 'message')— structured pino, context object first - Route pattern: factory function returning
Router(seesrc/routes/health.tsfor minimal example)
Development Commands
npm run build # tsc compile to dist/
npm run typecheck # type check only (no emit)
npm test # run all tests (vitest)
npx vitest run src/db.test.ts # run single test file
npm run dev # dev mode with tsx (no build needed)
npm run format:check # check formatting
npm run format:fix # fix formatting (also runs on pre-commit hook)Full tooling reference: make help shows all Makefile targets.
Docker Workflow
The Dockerfile uses a multi-stage build — TypeScript compilation happens inside Docker, so no local Node.js is required for building images.
./picoclaw.sh # one-click: env setup → build → docker run → smoke test
./picoclaw.sh up # build and start (no smoke test)
./picoclaw.sh test # smoke test running service
./picoclaw.sh stop-api # graceful stop via POST /control/stop
./picoclaw.sh down # docker stop
./picoclaw.sh logs # tail container logsAlternatively: make docker-build && make docker-run for manual control.
GHCR (Container Registry)
Images are published to ghcr.io/breakcafe/picoclaw. On main, tags are latest,
<version>, <version>-<commit>. On other branches, tags are dev, dev-<commit>,
dev-<branch>. Lambda variants append -lambda.
make ghcr-build # build standard image with GHCR tags
make ghcr-build-lambda # build Lambda image with GHCR tags
make ghcr-push # push standard image to GHCR
make ghcr-push-lambda # push Lambda image to GHCR
make ghcr-release # build + push all (standard + Lambda)
make ghcr-make-public # one-time: set package visibility to public (requires public repo or GitHub Team plan)Architecture
src/index.ts → process boot, directory init, shutdown signals
src/server.ts → Express app: middleware (json, requestId, build headers, request logger, db sync) → healthRoutes → authMiddleware → chat/task/admin/control
src/routes/chat.ts → POST /chat (SSE streaming), GET /chat, GET /chat/:id, GET /chat/:id/messages
src/routes/task.ts → task CRUD + POST /task/trigger + POST /task/check
src/routes/admin.ts → POST /admin/reload-skills, GET /admin/skills
src/routes/control.ts → POST /control/stop (graceful shutdown)
src/conversation-lock.ts → per-conversation mutex (prevents concurrent agent execution)
src/perf.ts → PerfTrace class for structured debug-level performance logging
src/agent-engine.ts → Claude Agent SDK query() wrapper, hooks, timeout via AbortController
src/mcp-inprocess.ts → In-process MCP server (type: 'sdk') — built-in picoclaw tools, no subprocess
src/mcp-server.ts → Stdio MCP server (legacy) — same tools, runs as standalone subprocess
src/task-utils.ts → Shared task scheduling utilities (computeNextRun, validateTaskOwnership)
src/db.ts → SQLite schema, CRUD operations, dual-path sync with dirty tracking
src/skills.ts → skill directory sync + .claude/settings.json bootstrap
src/config.ts → all env var defaults
src/types.ts → shared interfaces (Conversation, ScheduledTask, etc.)Boot sequence
entrypoint.sh
├── mkdir MEMORY_DIR, /data/store, skill directories
├── symlink ~/.claude → $MEMORY_DIR/.claude
└── write settings.json (if absent)
src/index.ts main()
├── ensureDataDirectories()
├── initDatabase()
├── ensureClaudeSettings() ← write settings.json (if absent, redundant with entrypoint)
├── syncSkills() ← three-tier skill sync (persist runtime skills, then merge)
├── loadManagedMcpServers() ← read $ORG_DIR/managed-mcp.json into memory cache
├── Startup diagnostics log ← paths, persona, MCP servers, model, SDK log level
└── Express listen on PORTSkill sync runs once at Node.js startup via syncSkills(). entrypoint.sh only creates directories; the Docker and local (npm start) paths both converge on the same TypeScript sync.
Request lifecycle
- HTTP hits Express router →
authMiddlewarevalidates Bearer token (skipped whenAPI_TOKENis unset) - Route handler resolves/creates conversation in SQLite
acquireConversationLock()prevents concurrent execution on same conversation (409 if busy)AgentEngine.run()wraps prompt inMessageStream, callsquery()- SDK spawns CLI subprocess → CLI spawns MCP Server (stdio)
- Agent executes tools; MCP tools read/write same SQLite via
PICOCLAW_DB_PATH query()yieldssystem/init(session_id, tools, mcp_servers),assistant(uuid),resultmessages- Route stores
session_id+last_assistant_uuidfor next resume - Conversation lock released in
finallyblock syncDatabaseToVolume()runs after response completes (skipped when no writes occurred)
Database schema (5 tables in src/db.ts)
conversations— id, session_id, last_assistant_uuid, status (idle/running), message_countmessages— id, conversation_id (FK), role, sender, content, created_atoutbound_messages— queued messages from MCPsend_message, delivered flagscheduled_tasks— id, conversation_id (FK), prompt, schedule_type/value, context_mode, next_run, statustask_run_logs— task_id (FK), run_at, duration_ms, status, result, error
MCP tools (defined in src/mcp-inprocess.ts)
The built-in picoclaw MCP server runs in-process (type: 'sdk'), sharing the
main PicoClaw database connection via db.ts imports. Tools:
send_message— queue message for HTTP caller during agent executionschedule_task— create cron/interval/once tasklist_tasks— list tasks (main session: all; non-main: own conversation only)pause_task/resume_task— pause/resume with ownership checkcancel_task— delete task with ownership checkupdate_task— modify prompt, schedule, context_mode
Volume mount semantics
| Mount | Runtime access | Agent cwd | Purpose |
/data/org | Read-only | No | Org persona, org skills, managed MCP config (optional) |
/data/memory | Read/Write | Yes (set as cwd) | User persona (CLAUDE.md), agent workspace, .claude/ SDK session state |
/data/store | Write (sync target) | No | Persistent SQLite (conversations, messages, scheduled tasks, run logs); synced from /tmp/messages.db after each response |
/tmp | Read/Write | No | Local runtime DB (ephemeral, fast) |
Persona and system prompt
PicoClaw uses a two-tier CLAUDE.md model for the agent's persona and system prompt,
implemented in src/agent-engine.ts:
| Tier | File | Code mechanism | Purpose |
| Org | $ORG_DIR/CLAUDE.md | loadOrgClaudeMd() → systemPrompt: { preset: 'claude_code', append } | Organization-wide policies, shared rules |
| User | /data/memory/CLAUDE.md | cwd: MEMORY_DIR + settingSources: ['project', 'user'] → SDK auto-discovers | Agent identity, capabilities, user-specific rules |
Assembly order (default): Claude Code preset → org CLAUDE.md (appended, if ORG_DIR is set) →
user CLAUDE.md (loaded by CLI from cwd). Both files are optional. All /data/* volumes
can be mounted as empty directories — the container starts and functions without a persona file.
System prompt override: Set SYSTEM_PROMPT_OVERRIDE env var to completely replace the
Claude Code preset + org CLAUDE.md with a custom system prompt. The user CLAUDE.md
(from cwd) is still loaded on top. When unset (default), the standard two-tier append
model is used.
Key implementation details:
settingSources: ['project', 'user']is required for the SDK to discover CLAUDE.md incwd. Without it, the SDK loads no filesystem settings (isolation by default).- When
ORG_DIRis empty or$ORG_DIR/CLAUDE.mddoes not exist,systemPromptisundefinedand the SDK falls back to the default Claude Code preset. The user CLAUDE.md still loads. - When
SYSTEM_PROMPT_OVERRIDEis set, it takes precedence over both the preset and org CLAUDE.md. The value is passed as a plain string tosystemPrompt, fully replacing the built-in Claude Code prompt. Use with caution — this removes built-in tool instructions. - The
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1'setting in.claude/settings.jsonalso enables CLAUDE.md discovery inadditionalDirectories(skill paths fromSKILLS_DIR).
Org MCP servers (managed-mcp.json)
When ORG_DIR is set and $ORG_DIR/managed-mcp.json exists, src/managed-mcp.ts
reads it at startup and caches the server configs in memory. These are merged
programmatically into query() mcpServers for every request — not via CLI
auto-discovery from /etc/claude-code/. CLI auto-discovery is avoided because it
triggers an enterprise MCP exclusion that prevents --mcp-config usage, breaking
the built-in picoclaw server and per-request dynamic servers.
MCP servers come from three sources, merged with this priority:
managed-mcp.json(from$ORG_DIR) — org-level, loaded bymanaged-mcp.ts- Built-in
picoclaw— always present, protected (cannot be overridden) - Per-request
mcp_servers— passed viaPOST /chatbody (can override managed same-name)
Adding a new route
- Create
src/routes/myroute.tswithexport function myRoutes(): Router - Mount in
src/server.tsafterauthMiddleware(or before, if no auth needed) - Add test in
src/routes/myroute.test.tsusing supertest (seeserver.test.tsfor pattern)
Adding an MCP tool
- Add tool definition in
src/mcp-server.tsunder theserver.tool()section - Use
z(zod) for input validation - Read/write via the shared SQLite
dbinstance - Tool is auto-discovered by agent as
mcp__picoclaw__<tool_name>
Dynamic MCP servers (per-request)
POST /chat accepts an optional mcp_servers field that lets callers attach
additional MCP servers to a specific request. These are merged with the built-in
picoclaw MCP server and passed to the SDK's query() call.
Supported transport types (maps to SDK McpServerConfig):
| Transport | Required fields |
http | url (and optional headers) |
sse | url (and optional headers) |
stdio | command (and optional args, env) |
Example request with an HTTP MCP server:
{
"message": "请帮我分析一下最近一周的支出情况",
"mcp_servers": {
"finance": {
"type": "http",
"url": "http://example.com/mcp-server/mcp"
}
}
}The agent will see tools from all configured MCP servers. Tool names follow the
pattern mcp__<server_name>__<tool_name> — so the example above exposes
mcp__finance__* tools alongside the built-in mcp__picoclaw__* tools.
Dynamic MCP context (per-request auth)
POST /chat accepts an optional mcp_context field that injects per-request
auth headers, env vars, or args into existing MCP servers. Context is applied
after the three-way merge — it works with org-managed, per-request, and
built-in servers (except picoclaw, which is protected).
| Field | Applies to | Behavior |
headers | http/sse servers | Merged with static headers (context overrides same-key) |
env | stdio servers | Merged with static env (context overrides same-key) |
args | stdio servers | Appended to existing args array |
Example request with per-user auth context:
{
"message": "查询我的订单",
"mcp_context": {
"finance": {
"headers": {
"Authorization": "Bearer user-token-123",
"X-Tenant-Id": "tenant-abc"
}
}
}
}Warning cases:
- Server name not found after merge -> warning (context ignored)
- Reserved name
picoclaw-> warning (context ignored) - Type mismatch (headers for stdio, env for http) -> warning per mismatched field
- Auth-related headers (
Authorization,X-Api-Key, etc.) are scrubbed from debug logs
Non-Negotiable Principles
- Memory and conversation history are core — never treat as optional. Cross-request personalization depends on persistent mounted paths.
- Persistent data model —
MEMORY_DIRandSTORE_DIRmust be preserved on mounted volumes.ORG_DIRis read-only and optional. SDK session state lives at$MEMORY_DIR/.claude/(no separateSESSIONS_DIR). - Graceful stop must sync data — both
POST /control/stopandSIGTERM/SIGINTtriggersyncDatabaseToVolume()→closeDatabase()→ exit. - SDK version alignment —
@anthropic-ai/claude-agent-sdk:0.2.86,@modelcontextprotocol/sdk:1.28.0. Do not downgrade. - Dual-DB sync is the only safe write path — never write directly to
/data/store/messages.db. Always operate on/tmp/messages.dband let sync copy it.
Key Environment Variables
| Variable | Default | Code location |
ANTHROPIC_BASE_URL | (empty; SDK defaults to https://api.anthropic.com) | Used by SDK internally; set for third-party API proxies |
ANTHROPIC_API_KEY | (required) | Used by SDK internally |
APP_VERSION | 1.0.0 | src/config.ts → health response, X-Build-Version header; overridden by BUILD_VERSION Docker ARG |
API_TOKEN | (empty) | src/config.ts → auth middleware. When unset, auth is disabled (all endpoints public). |
PORT | 9000 | src/config.ts |
MAX_EXECUTION_MS | 300000 | src/config.ts → AbortController timeout |
MEMORY_DIR | /data/memory | src/config.ts → agent cwd, persona, .claude/ session state |
STORE_DIR | /data/store | src/config.ts → persistent SQLite sync target |
ASSISTANT_NAME | Pico | src/config.ts → agent display name, transcript archiving |
LOG_LEVEL | info | src/logger.ts → pino log level (debug, info, warn, error) |
SESSION_END_MARKER | [[PICOCLAW_SESSION_END]] | src/config.ts → chat response |
ORG_DIR | (empty) | src/config.ts → org persona, org skills, managed MCP config |
SKILLS_DIR | $ORG_DIR/skills or /data/skills (fallback) | src/config.ts → org skills directory |
PICOCLAW_DB_PATH | /tmp/messages.db | MCP server env var (set by agent-engine; NANOCLAW_DB_PATH as fallback) |
PICOCLAW_CONVERSATION_ID | per-request | MCP server env var (set by agent-engine; NANOCLAW_CONVERSATION_ID as fallback) |
PICOCLAW_IS_MAIN | 1 | MCP server env var — enables cross-conversation task management |
BUILD_COMMIT | unknown | src/config.ts → git commit hash, injected at Docker build time |
BUILD_TIME | unknown | src/config.ts → build timestamp, injected at Docker build time |
CLAUDE_MODEL | (empty; CLI default) | src/config.ts → model for query() (full ID or short name) |
CLAUDE_FALLBACK_MODEL | (empty) | src/config.ts → fallback model on primary failure |
SYSTEM_PROMPT_OVERRIDE | (empty) | When set, fully replaces Claude Code preset + org CLAUDE.md |
SDK_LOG_LEVEL | off | off or debug — pipes SDK stderr through pino at debug level |
OUTBOUND_TTL_DAYS | 7 | Days to keep delivered outbound messages before cleanup |
TASK_LOG_RETENTION | 100 | Max task run logs kept per task (oldest pruned on sync) |
CLEANUP_INTERVAL_SEC | 60 | Min seconds between cleanup runs during DB sync. 0 = every sync. |
Performance Debug Logging
Set LOG_LEVEL=debug to enable structured performance traces. Three scopes:
| Tag | Source file | What it measures |
[PERF:BOOT] | src/index.ts | Startup sequence: dirs, DB init, skills sync, MCP load |
[PERF:AGENT] | src/agent-engine.ts | Per-request agent lifecycle: pre-query setup, sdkInit, firstTextDelta, sdkResult |
[PERF:DB] | src/db.ts | Database sync: cleanup, WAL checkpoint, file copy |
Each trace is a single JSON log entry with scope, totalMs, and a steps array.
Health probe (GET /health) logs are also at debug level to avoid log noise.
Common Gotchas
- Reserved MCP server name:
picoclawis reserved for the built-in MCP server. Per-requestmcp_serversentries using this name are rejected with a warning. managed-mcp.json entries namedpicoclaware rejected at load time with a warning. - Enterprise MCP exclusion: Claude Code CLI rejects
--mcp-configwhen/etc/claude-code/managed-mcp.jsonexists. PicoClaw avoids this by loading managed-mcp.json programmatically viasrc/managed-mcp.tsinstead of copying it to/etc/claude-code/. Never place files in/etc/claude-code/. - ESM
.jsin imports: TypeScript compiles.ts→.jsbut import paths must already say.js. Forgetting this causes runtimeERR_MODULE_NOT_FOUND. - Built-in MCP is in-process:
src/mcp-inprocess.tsruns in the PicoClaw process (not as a subprocess) usingcreateSdkMcpServer()withtype: 'sdk'. It directly importsdb.tsfunctions — no separate SQLite connection. SetPICOCLAW_MCP_SERVER_PATH=dist/mcp-server.jsto fall back to stdio subprocess mode. - Dual-DB sync with dirty tracking: Runtime operates on
/tmp/messages.db(fast local).syncDatabaseToVolume()is called after every HTTP response but skips WAL checkpoint and file copy when no writes occurred since the last sync (dirty flag). Cleanup queries (cleanupStaleData) are throttled to run at most once perCLEANUP_INTERVAL_SEC(default 60s). Shutdown path usesforce=trueto guarantee a final sync regardless of dirty state. Never write directly to the volume path. - Pre-compact hook:
agent-engine.tsarchives transcripts to/data/memory/conversations/when Claude compacts context. This rarely triggers in PicoClaw's request-driven model — only when a singlequery()execution approaches the context window limit. - MessageStream wrapping: Prompts are wrapped in an
AsyncIterable(not passed as strings) to prevent the SDK from prematurely closing stdin and killing subagents. SeeMessageStreamclass inagent-engine.ts. - Isolated task conversations: When
context_mode=isolated, the MCP server callsensureConversationExists()before creating a task to avoid FK constraint violations. formatvsformat:fix: Both run Prettier write. The pre-commit hook callsformat:fix. Useformat:checkto verify without modifying.- Auto-memory is non-functional: Claude Code's auto-memory feature (
MEMORY.mdauto-generation) is gated behind an internal feature flag (tengu_herring_clock, defaultfalse) in the CLI. In SDK/non-interactive mode, the auto-memory system prompt is never injected, soMEMORY.mdis never written — regardless of theCLAUDE_CODE_DISABLE_AUTO_MEMORYsetting.entrypoint.shsets up a symlink from the SDK's auto-memory path to/data/memory/as a forward-compatibility measure, but the feature is currently inert. Cross-session memory must be implemented in the persona (CLAUDE.md) by instructing the agent to read/write files in/data/memory/explicitly. - Zod v4 + MCP SDK:
@modelcontextprotocol/sdk@1.28.0supportszod ^3.25 || ^4.0, so MCP tool schemas insrc/mcp-server.tsuse the standardimport { z } from 'zod'(v4). If the MCP SDK is ever downgraded below 1.27.1, MCP tools with parameters will break at runtime withkeyValidator._parse is not a functionbecause older SDK versions only support zod v3. - Org skills are authoritative: User skills (
$MEMORY_DIR/skills/) are additive only — they cannot override org or built-in skills of the same name. This ensures org policies remain the authoritative source. additionalDirectoriesis NOT skill discovery:discoverAdditionalDirectories()passes org skill subdirectory paths toquery()asadditionalDirectories. This only grants file access and CLAUDE.md discovery in those paths — it does NOT make them skill sources. Skill discovery always comes from.claude/skills/viasyncSkills(). Adding a new skill directory toadditionalDirectorieswill not register its SKILL.md files.
Change Guardrails
After any code change, verify:
npm run build && npm test # must pass
npm run format:check # must passAlso check:
- OpenAPI spec (
docs/api/openapi.yaml) updated if API contract changed CHANGELOG.mdupdated for user-visible changes- Stop path still syncs data and exits cleanly
Docs
- @docs/CONFIGURATION_REFERENCE.md — all env vars, API endpoints, volumes, and controls at a glance
- @docs/SERVERLESS_API_DEPLOYMENT_GUIDE.md — full operations and deployment manual
- @docs/API_INTEGRATION_GUIDE.md — downstream HTTP API integration guide
- @docs/SKILLS_AND_PERSONA_GUIDE.md — skill authoring and persona configuration
- @docs/SECURITY.md — HTTP API trust model and deployment hardening
- @docs/DESIGN_RATIONALE.md — architectural decisions and first-principles reasoning
- @docs/SDK_DEEP_DIVE.md — Claude Agent SDK internals (query, session resume, hooks)
docs/api/openapi.yaml/openapi.json— API specification (validated bysrc/openapi.test.ts)docs/api/postman_collection.json— API smoke testing collection
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

