firat.sertgoz 09ac3c8af0 feat(memory): memory-save guidance + always-on MEMORY.md prompt lane (#7185) (#7365)
* feat(memory): tell the model when to save durable user facts

Nothing in the system prompt explains that persistent memory exists, that
the memory block it sees came from earlier conversations, or when a stated
user preference is worth saving. Add a `memory_protocol.md` prompt asset,
appended in memory on every resolve like the self-knowledge section, so
existing installs get it rather than only freshly seeded ones (#7185).

Also point `ironclaw.memory.write`'s prompt doc and both provider manifest
descriptions at the durable-fact use case, so the tool description agrees
with the protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(memory): inject MEMORY.md into every prompt without an FTS match

Both proactive-recall lanes are full-text search over the current turn's
query, so a fact saved in conversation A only reached conversation B when
B's opening message happened to share vocabulary with it (#7185).

Add a `read_curated` memory lifecycle hook and `MemoryService::read_curated`,
implemented by the native provider as a plain read of the scope's `MEMORY.md`
(absent or empty = an empty lane, not an error). The host queries it on every
run with no query at all and admits it FIRST, ahead of the search lanes.

The lane is split on line boundaries into per-snippet-sized chunks rather
than admitted as one oversized snippet: a snippet's model-visible text is
validated as a 512-byte `LoopSafeSummary`, which is also where the prompt
denylist runs, so a single wide snippet would mean denylist-checking only the
head of the document. Chunks are capped at 4 snippets / 2 KiB — half the
aggregate — so the standing document can neither starve nor be starved by the
search lanes, and a clipped document is marked truncated.

The hook is opt-in per manifest, so mem0 (no standing-document concept) is
never called on it and is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(memory): pin cross-conversation recall and the memory-write gate

Integration (real composition + libSQL): conversation A's model saves a
durable fact with append mode, and conversation B — opening on an unrelated
topic, sharing no content word with the fact, making no memory tool call —
still finds it in its system prompt. Another user's MEMORY.md on the same
composite does not. This is the case the existing proactive-recall scenario
cannot cover, because both search lanes need the query to match.

The lifecycle scenario now counts read_curated too, so "a full declaration
drives every hook" stays true rather than silently skipping the new one.

Composition: pin the approval posture the issue is really about — with the
shipping default settings (global auto-approve on) ironclaw.memory.write is
allowed, so saving a fact does not stop the turn on a prompt; with
auto-approve deliberately off it still gates. The approval seam is
per-capability (the gate policy sees the descriptor, never the invocation
input), so "ungated for curated targets only" is not expressible there, and
an unconditionally ungated save tool would override the choice of exactly
the users the gate currently protects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(architecture): raise the extension-contracts size ceiling for read_curated

The `read_curated` lifecycle hook adds one enum variant, its wire token, its
`ALL` entry, and a doc comment to ironclaw_extension_contracts — 13 lines of
declaration vocabulary. The lane's behavior (reading the standing document,
line-aligned chunking, budgets, sanitization) lives in the native provider
and ironclaw_host_runtime, so no logic reached the contracts tier.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(memory): teach the model how to phrase a memory worth keeping

The memory protocol told the model *when* to save a durable user fact but
nothing about what a good one looks like, and three failure modes showed up
in practice:

- A memory saved as an imperative ("Always respond concisely") is re-read as
  part of the prompt on every later turn — and the new always-on lane
  re-injects it every turn — so it becomes a standing directive that can
  override what the user is asking for right now. Memories must be
  declarative facts ("User prefers concise responses").
- Task progress, session outcomes, PR numbers, and commit SHAs are stale
  within days and crowd out the durable facts the lane exists to surface.
- With no priority framing the model saves whatever is nearest to hand
  rather than the fact that stops the user repeating themselves.

Adds those three rules to `memory_protocol.md` and the declarative-form and
staleness rules to the shared `ironclaw.memory.write` prompt doc, and
clarifies that honoring a forget request means actually rewriting the
document rather than only acknowledging it (raised in review — `write` with
`append` unset replaces the document, so the guidance is executable).

Two asset tests pin it: one for each doctrine phrase models actually copy
(including both halves of the good/bad example pair), one keeping the
protocol inside an 18-line budget, since it is appended to every prompt on
every turn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(memory): address review findings on the always-on recall lane

Four bot-review findings on #7365, all of them real:

**The memory protocol claimed a surface a disabled deployment does not
have.** `MEMORY_PROTOCOL_PROMPT` was appended unconditionally, but a
`Disabled` memory binding resolves to no provider and registers no memory
package, so the model sees no `ironclaw.memory.*` tools at all — the prompt
was telling it persistent memory exists and to call tools absent from its
surface. Composition now carries resolved memory availability into prompt
assembly, gated the same way the tool-disclosure protocol is gated on the
bridge tools existing. The three positional `bool`s on
`DefaultSystemPromptIdentitySource::try_new` become a named
`SystemPromptProtocols` struct: each one licenses the prompt to claim a
capability, and a swapped pair would fail silently. Pinned by a
production-caller test asserting the unbound prompt carries neither the
section nor a memory tool id, with a bound arm so it cannot pass vacuously.

**Two consecutive saved facts ran together.** The backend append is
byte-exact and the protocol asks for one self-contained line per fact, so
"likes tea" then "lives in Berlin" persisted as `likes tealives in Berlin`
— and the curated lane splits `MEMORY.md` on line boundaries, so both facts
reached later turns as one corrupted fact. The native service now
terminates every appended entry with exactly one newline. Regression test
drives two guided appends through the real service and asserts the curated
lane reads back two lines.

**The curated split chunked the whole document before truncating.**
`MEMORY.md` is user-controlled and re-read on every run, so a large standing
document allocated a `String` plus a snippet clone per ~400 bytes on the
retrieve-before-run path and then discarded all but four. `split_curated_text`
now takes a chunk cap and stops there; the caller passes `budget + 1`, the
one extra chunk being what proves the document was longer than admitted.

**`tests/CLAUDE.md` omitted the new scenario.** Added to the Memory section;
group totals corrected 51 → 53 against the tree.

Not applied: CodeRabbit also asked for a `coverage-floor.toml` row for the
new scenario, but that file gates per-crate coverage and has no per-scenario
rows. IronLoop asked to bound the curated *read* itself; that needs a
byte-limited primitive on the repository/filesystem contract and is a
separate change — the host-side cap above bounds the work that follows it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(memory): compose the always-on lane over the document-store read op

The bespoke MemoryLifecycleHook::ReadCurated hook was unearned API: it
duplicated a read every document-backed provider already serves as the
ironclaw.memory.read tool, just under a new lane-specific contract. Replace
it with a general MemoryService::read_document trait method (fail-closed
`unavailable` default) that native and mem0 implement by delegating to
their existing `read`. The host composes the always-on curated lane itself
out of an ordinary document read of MEMORY.md, run unconditionally
(gated only by the existing memory-disabled context-profile check, never by
a manifest declaration) — the whole point of #7185 is that a user's
standing facts do not depend on a provider opting in to a curated-specific
hook. ReadCurated is removed everywhere: the enum variant, both manifests'
lifecycle tokens, and the extension-contracts size-ceiling bump it required.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(memory): rename curated-lane test off retired vocabulary

The reborn_memory_retired_vocabulary ratchet (landed on main) pins the
literal `document_store` at zero occurrences; the curated-lane test name
tripped it after the merge. Rename only — behavior unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(memory): name the rewrite mode on the forget path, cover the gate at the caller

Three review findings on #7365, all narrow:

- `memory_protocol.md` established `append: true` as the save mode, then
  told the model to "rewrite the memory document" for a forget request
  without naming the mode. A model carrying `append: true` forward appends
  the correction and leaves the entry the user asked to drop, which the
  always-on lane then re-injects alongside it. Say `append: false`, and
  pin both modes in the asset test.
- The persistent-memory gate is composed in `runtime.rs` from whether a
  provider actually resolved; the unit tests only prove the flag is
  honored once someone sets it. Assert at the production construction
  site that a runtime with a bound provider really does inject the
  protocol.
- `tests/CLAUDE.md`: the §3 summary still counted five memory scenarios
  against the seven §3.4 lists, and the lifecycle row did not mention
  that the curated standing-document read runs ungated by design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(memory): serve the standing document inside native's long-term lane (#7185)

A fact the user states in one conversation was invisible in the next one
that opened on an unrelated subject. Both proactive lanes were full-text
search over the current turn's message, so recall depended on the reader
happening to reuse the writer's vocabulary.

The native provider now serves its standing `MEMORY.md` at the head of its
own `read_long_term`, ahead of the full-text hits and independent of the
query. That is the lane's stated job — "the user's general, durable
memory" — and search alone cannot deliver it.

Provider-internal, deliberately. An earlier revision made this a third
host lane composed over a new `MemoryService::read_document` trait method;
both are gone. `MemoryService` is byte-identical to main again and the
host's prompt-context service is back to two lanes with no document paths
in it. What the host sees is ordinary lane snippets it cannot tell from
search hits, which is what keeps the whole safety envelope — scope filter,
untrusted envelope, prompt denylist, per-snippet cap — applying unchanged.

Moving with the retrieval, because they are the same decision:

- The line-boundary splitter, the 4-snippet budget, and the truncation
  marker are now this provider's policy. A whole document as one wide
  snippet would be denylist-checked only at its head, so it is cut into
  chunks that each pass the host's per-snippet contract; a line carrying a
  denylisted secret is dropped on its own and the surrounding facts still
  reach the model.
- The marker reserves its own room instead of pushing a chunk past its
  cap, so a clipped document cannot read as a complete one.
- Append-mode writes terminate each entry with exactly one newline. The
  backend append is byte-exact, so without it two correct guided saves
  ("drinks tea", then "lives in Berlin") persist as one run-on line and
  reach later turns as one corrupted fact.
- A memory-disabled context profile short-circuits before the document
  read, not only before the search, so a disabled profile still issues no
  provider read at all.

mem0 is untouched: it serves no standing document, so its lane keeps its
existing behavior and issues no extra read. That also takes the #7505
target-alias divergence off this path — it remains a real tool-path
contract issue, but no lane code is involved in it any more.

The libSQL integration scenario is unchanged and is the behavior-neutrality
proof: the same user-facing property, asserted through real composition,
survived the mechanism moving from the host into the provider. The
lifecycle scenario returns to pure declared-hook counting, because there is
no host-composed document read left to except from it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(memory): let the memory extension ship its own model guidance (#7185)

Nothing ever told the model that persistent memory exists or when a stated
preference is worth saving, so it usually never called
`ironclaw.memory.write` at all. That guidance now ships with the memory
extension instead of the loop tier.

`[memory]` gains one optional field, `guidance_doc`, naming a bundled asset
in the provider's own package. memory-native declares
`prompts/memory-guidance.md`; composition appends whatever the BOUND
provider declares and writes none of it.

The text was previously `ironclaw_loop_host`'s `memory_protocol.md`, which
was the wrong owner: it names concrete `ironclaw.memory.*` tools and
describes one provider's recall behavior. mem0's recall is search-first and
it serves no standing document, so native's "save it to `memory`, it comes
back every turn" advice would be actively misleading under a mem0 binding —
and there was no way to say so short of host code branching on provider
identity. Declaring the doc in the manifest that already declares the tools
puts the wording next to the thing it describes, and makes "ships no
guidance" a first-class answer. mem0 declares none, with the reason in its
manifest; absent means nothing is appended, never a fallback to another
provider's wording.

Consequences worth naming:

- `SystemPromptProtocols` carries provider CONTENT (`Option<String>`), not
  a host flag, and `ironclaw_loop_host` is byte-identical to main again —
  the loop tier owns no memory prompt text.
- Two conditions gate the append, both necessary: a provider must actually
  be resolved (a `Disabled` binding registers no package, so the model sees
  no memory tools and must not be told they exist), and that provider must
  declare guidance.
- The ref is the existing validated `CapabilityProfileSchemaRef`, the same
  newtype `prompt_doc_ref` uses, so a path escaping the package fails the
  manifest parse. A valid ref no bundled provider claims is fail-quiet:
  guidance carries no authority and gates nothing, so an unknown or future
  declaration appends nothing rather than failing a boot.
- The host resolves the ref through `ironclaw_memory_native`'s public API
  rather than `include_str!`-ing its asset tree. The first cut copied the
  inline-schema precedent and `reborn_cross_crate_include_scan` correctly
  rejected it — §11.2.7 is shrink-only. Exporting the ref and the text from
  one file also makes it impossible for the manifest and the asset to drift
  apart.
- `ironclaw_extension_contracts`' §11.2.3 size ceiling is raised 7_892 ->
  7_947 for the field, its doc, and two parse tests. Declaration vocabulary
  only: the text ships with the package, resolution lives in
  ironclaw_host_runtime, assembly stays in composition.

Guidance content is unchanged from the reviewed version, including the
`append: false` rewrite mode on the forget path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(memory): pin the guidance content where the guidance lives

The guidance-content tests moved with the asset. Composition now asserts
only what composition owns — that the bound provider's declared text is
appended verbatim, survives a user-edited SYSTEM.md, is never seeded into
the user's file, and is absent with no provider bound — against its own
fixture rather than any real provider's wording. Asserting native's exact
sentences there was testing one provider through the layer that is supposed
not to know about it, and it silently became the only thing keeping the
write-quality doctrine alive.

The content pins themselves are re-homed in the memory-native package,
where the text ships: the tool ids it names, the curated target and both
write modes, the never-save carve-out, the declarative-form rule with its
worked example pair, the staleness skip-list, the priority framing, and the
heading + line budget it has to keep to be worth appending on every turn.
Each of those is a failure the compiler cannot see.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(memory): keep the standing document out of its own lane's search half

Two review findings on the curated prefix, both real:

- A query whose words happen to match `MEMORY.md` re-admitted the standing
  document as a full-text hit behind the curated copy the model can already
  see. That spent a second snippet slot on a duplicate and displaced a
  different document that matched. The lane now excludes `MEMORY_PATH` from
  the search remainder for the same reason it excludes thread scratch: the
  prefix already owns it. Regression drives `max_snippets = 2` with both
  documents matching, so the displacement would be visible.
- A single line longer than a chunk became an oversized chunk. Previously
  the host clipped it and stamped the marker; now that the prefix rides the
  ordinary lane, the host sanitizes it exactly like a search hit and
  truncates SILENTLY — so an over-long line reached the model shortened
  with nothing saying so, and the documented per-chunk limit was not
  actually enforced. The splitter now clips such a line at a char boundary
  and marks it before admission, and `clip_and_mark` is the one place that
  reserves room for the marker.

One existing fixture moved off `MEMORY.md` onto an ordinary path:
`native_context_retrieve_excludes_thread_scratch_from_long_term` used the
standing document as its generic "durable doc", which after the first fix
would have tested the new exclusion instead of the thread-scratch one it
exists to pin. Intent and assertions unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): keep composition's measured mass honest by splitting its prompt tests

`Fast deterministic checks` was red on the composition budget gate: 41868
production LOC against an effective ceiling of 41732, 136 over. The gate
counts LOC and cannot parse an inline `#[cfg(test)]` module out of an
otherwise-production file, so this branch's prompt-assembly tests were
being counted as assembly code.

Split `root/default_system_prompt.rs`'s inline test module verbatim into a
`root/default_system_prompt/tests.rs` sibling, which the gate excludes —
the same move #7151-era fixes used for `runtime_context.rs` and
`host/run_context.rs`. No ceiling raise: composition now measures 41393,
which is 302 LOC BELOW main, so the branch ratchets the crate down rather
than spending budget on test code.

Not a rename of the problem: the tests are unchanged and all 10 still pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(memory): resolve guidance from the bound provider's own bundled assets

Review finding on #7365: guidance_doc was declared per provider but the
host resolved it through a hard-coded match against memory-native's
exported constants — a non-native provider declaring guidance would
silently resolve to nothing.

Resolution is now generic host code over provider-supplied data: each
provider crate exports its own MEMORY_GUIDANCE_ASSETS table, the shared
bundle constructor resolves the declared ref against the bundled
provider's own table at construction time, and BundledMemoryProvider
carries the resolved content. host_runtime cannot name the mem0 crate
(sanctioned-residue dependency ratchet), so mem0's bundle constructor
takes the table as a parameter and composition — which legitimately
depends on both providers — supplies it at the call site.

Failure semantics strengthen from fail-quiet to fail-loud: a declared
ref that does not resolve within the provider's own assets is a
manifest/asset desync and fails bundle construction, same posture as
the existing missing-[memory] check. Absent guidance_doc remains a
normal no-guidance state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(ci): ratchet the composition mass ceiling down to this tree

The memory-save guidance and its content pins moved out of composition
into the memory-native package that owns them, and the prompt tests were
split out of the production file. The gate's NUDGE fired at 283 LOC of
slack, and its C11 self-test fails a ceiling that no longer binds — so
lock the improvement in rather than bank it as headroom.

Measured on the merged tree with scripts/ci/check-composition-budget.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): give the QA replay job the stack its own suite documents

reborn_qa_recorded_behavior's module docs require RUST_MIN_STACK=67108864:
its replay tests build a full Reborn runtime and drive whole turns, so the
composed debug async frames sit within a few percent of whatever stack they
get. The root-tests and group-suite jobs already set exactly this value and
cite this binary's docs; the job that actually runs it never did.

Measured on this suite (macOS, debug): it overflows at 1984 KiB and clears
at 2048 KiB — main and this branch to the byte, so the libtest default IS
the boundary and any layout shift decides it. That is the same 'unrelated
layout shift' the group-suite comment already records.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): move the composition mass record with its ceiling

The absolute-mass ratchet is two committed numbers that must agree: the
gate manifest's loc_ceiling and the arch-test record that asserts it still
binds. The previous commit lowered the ceiling for this branch's eviction
but left the record at its pre-eviction value, so the record then exceeded
the effective ceiling and reborn_restructure_baselines went red.

Both now sit at 41533, re-measured on the merged tree with
scripts/ci/check-composition-budget.sh after merging current main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(memory): refresh golden payloads for the memory.write description

The memory tool's description now names when to save a durable user fact,
so the two golden payloads carrying the tool surface move with it — the
sentence itself, and the surface sha256 derived from those descriptions.

No other bytes changed in either snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 14:38:59 +00:00

IronClaw

IronClaw

Your secure personal AI assistant, always on your side

License: MIT OR Apache-2.0 Telegram: @ironclawAI Reddit: r/ironclawAI gitcgr

English | 简体中文 | Русский | 日本語 | 한국어

Quick StartPhilosophyFeaturesInstallationConfigurationSecurityArchitecture


Quick Start

Choose an ironclaw-v* tag from the Releases page, then install it on macOS, Linux, or Windows/WSL. Replace X.Y.Z with the selected version, including any prerelease suffix:

IRONCLAW_RELEASE_TAG=ironclaw-vX.Y.Z
curl --proto '=https' --tlsv1.2 -LsSf \
  "https://github.com/nearai/ironclaw/releases/download/${IRONCLAW_RELEASE_TAG}/ironclaw-installer.sh" | sh

Then run the guided setup:

ironclaw onboard

Choose an LLM provider, enter its API key in the hidden prompt, and accept the default model or enter another one. IronClaw provisions its local configuration, encrypted credential store, and WebUI login token. On macOS and Linux it also installs and starts the background service, then prints a link that opens the WebUI.

Use ironclaw status to check the service and print the login link again. Windows users can start the WebUI in the foreground with ironclaw serve. See Installation for Windows installers and source builds.

Philosophy

IronClaw is built on a simple principle: your AI assistant should work for you, not against you.

In a world where AI systems are increasingly opaque about data handling and aligned with corporate interests, IronClaw takes a different approach:

  • Your data stays yours - All information is stored locally, encrypted, and never leaves your control
  • Transparency by design - Open source, auditable, no hidden telemetry or data harvesting
  • Self-expanding capabilities - Build new tools on the fly without waiting for vendor updates
  • Defense in depth - Multiple security layers protect against prompt injection and data exfiltration

IronClaw is the AI assistant you can actually trust with your personal and professional life.

Features

Security First

  • WASM Sandbox - Untrusted tools run in isolated WebAssembly containers with capability-based permissions
  • Credential Protection - Secrets are never exposed to tools; injected at the host boundary with leak detection
  • Prompt Injection Defense - Pattern detection, content sanitization, and policy enforcement
  • Endpoint Allowlisting - HTTP requests only to explicitly approved hosts and paths

Always Available

  • Multi-channel - REPL, HTTP webhooks, WASM channels (Telegram, Slack), and web gateway
  • Docker Sandbox - Isolated container execution with per-job tokens and orchestrator/worker pattern
  • Web Gateway - Browser UI with real-time SSE/WebSocket streaming
  • Routines - Cron schedules, event triggers, webhook handlers for background automation
  • Heartbeat System - Proactive background execution for monitoring and maintenance tasks
  • Parallel Jobs - Handle multiple requests concurrently with isolated contexts
  • Self-repair - Automatic detection and recovery of stuck operations

Self-Expanding

  • Dynamic Tool Building - Describe what you need, and IronClaw builds it as a WASM tool
  • MCP Protocol - Connect to Model Context Protocol servers for additional capabilities
  • Plugin Architecture - Drop in new WASM tools and channels without restarting

Persistent Memory

  • Hybrid Search - Full-text + vector search using Reciprocal Rank Fusion
  • Workspace Filesystem - Flexible path-based storage for notes, logs, and context
  • Identity Files - Maintain consistent personality and preferences across sessions

Installation

The Releases page provides pre-built binaries and installers.

Install via Windows Installer (Windows)

Open the selected ironclaw-v* release, download ironclaw-x86_64-pc-windows-msvc.msi, and run it.

Install via PowerShell script (Windows)
$IronClawReleaseTag = "ironclaw-vX.Y.Z"
irm "https://github.com/nearai/ironclaw/releases/download/$IronClawReleaseTag/ironclaw-installer.ps1" | iex
Install via shell script (macOS, Linux, Windows/WSL)
IRONCLAW_RELEASE_TAG=ironclaw-vX.Y.Z
curl --proto '=https' --tlsv1.2 -LsSf \
  "https://github.com/nearai/ironclaw/releases/download/${IRONCLAW_RELEASE_TAG}/ironclaw-installer.sh" | sh
Build and install from source

Source builds require Rust 1.96+ and Node.js 22+ with Corepack/pnpm.

git clone https://github.com/nearai/ironclaw.git
cd ironclaw
corepack enable pnpm
cargo install --locked --path crates/app/ironclaw_cli

Configuration

ironclaw onboard is the primary configuration path. It writes Reborn state under $HOME/.ironclaw/reborn by default, stores the selected LLM credential in the encrypted local secret store, and preserves existing configuration when it is run again.

Inspect the current setup with:

ironclaw status
ironclaw models status
ironclaw config list

To switch providers after onboarding, select the route and then store its API key using the hidden prompt:

ironclaw models set-provider openai --model gpt-5-mini
ironclaw config set openai.api_key

Additional settings use the same command. For example:

ironclaw config set google.client_id YOUR_CLIENT_ID
ironclaw config set google.client_secret
ironclaw config set google.redirect_uri YOUR_REDIRECT_URI
ironclaw config set webui.token --rotate

Secret values never accept a positional argument; IronClaw prompts for them without echoing the value. Channels such as Slack and Telegram have no configuration-file settings and no CLI enablement key: install the extension and complete its setup on the WebUI Extensions page, which is what makes the route serve.

Configuration writes never restart the service automatically. Run ironclaw service restart after a change that affects the running service, and use ironclaw config set --help for the complete list of supported keys.

Security

IronClaw implements defense in depth to protect your data and prevent misuse.

WASM Sandbox

All untrusted tools run in isolated WebAssembly containers:

  • Capability-based permissions - Explicit opt-in for HTTP, secrets, tool invocation
  • Endpoint allowlisting - HTTP requests only to approved hosts/paths
  • Credential injection - Secrets injected at host boundary, never exposed to WASM code
  • Leak detection - Scans requests and responses for secret exfiltration attempts
  • Rate limiting - Per-tool request limits to prevent abuse
  • Resource limits - Memory, CPU, and execution time constraints
WASM ──► Allowlist ──► Leak Scan ──► Credential ──► Execute ──► Leak Scan ──► WASM
         Validator     (request)     Injector       Request     (response)

Prompt Injection Defense

External content passes through multiple security layers:

  • Pattern-based detection of injection attempts
  • Content sanitization and escaping
  • Policy rules with severity levels (Block/Warn/Review/Sanitize)
  • Tool output wrapping for safe LLM context injection

Data Protection

  • All data stored locally in IronClaw's application state
  • Secrets encrypted with AES-256-GCM
  • No telemetry, analytics, or data sharing
  • Full audit log of all tool executions

Architecture

┌────────────────────────────────────────────────────────────────┐
│                          Channels                              │
│  ┌──────┐  ┌──────┐   ┌─────────────┐  ┌─────────────┐         │
│  │ REPL │  │ HTTP │   │WASM Channels│  │ Web Gateway │         │
│  └──┬───┘  └──┬───┘   └──────┬──────┘  │ (SSE + WS)  │         │
│     │         │              │         └──────┬──────┘         │
│     └─────────┴──────────────┴────────────────┘                │
│                              │                                 │
│                    ┌─────────▼─────────┐                       │
│                    │    Agent Loop     │  Intent routing       │
│                    └────┬──────────┬───┘                       │
│                         │          │                           │
│              ┌──────────▼────┐  ┌──▼───────────────┐           │
│              │  Scheduler    │  │ Routines Engine  │           │
│              │(parallel jobs)│  │(cron, event, wh) │           │
│              └──────┬────────┘  └────────┬─────────┘           │
│                     │                    │                     │
│       ┌─────────────┼────────────────────┘                     │
│       │             │                                          │
│   ┌───▼─────┐  ┌────▼────────────────┐                         │
│   │ Local   │  │    Orchestrator     │                         │
│   │Workers  │  │  ┌───────────────┐  │                         │
│   │(in-proc)│  │  │ Docker Sandbox│  │                         │
│   └───┬─────┘  │  │   Containers  │  │                         │
│       │        │  │ ┌───────────┐ │  │                         │
│       │        │  │ │Worker / CC│ │  │                         │
│       │        │  │ └───────────┘ │  │                         │
│       │        │  └───────────────┘  │                         │
│       │        └─────────┬───────────┘                         │
│       └──────────────────┤                                     │
│                          │                                     │
│              ┌───────────▼──────────┐                          │
│              │    Tool Registry     │                          │
│              │  Built-in, MCP, WASM │                          │
│              └──────────────────────┘                          │
└────────────────────────────────────────────────────────────────┘

Core Components

Component Purpose
Agent Loop Main message handling and job coordination
Router Classifies user intent (command, query, task)
Scheduler Manages parallel job execution with priorities
Worker Executes jobs with LLM reasoning and tool calls
Orchestrator Container lifecycle, LLM proxying, per-job auth
Web Gateway Browser UI with chat, memory, jobs, logs, extensions, routines
Routines Engine Scheduled (cron) and reactive (event, webhook) background tasks
Workspace Persistent memory with hybrid search
Safety Layer Prompt injection defense and content sanitization

Usage

# Check the background service and print the WebUI login link
ironclaw status

# Start an interactive terminal session
ironclaw repl

# Run one turn
ironclaw run --message "hello"

Development

# Format code
cargo fmt

# Lint
cargo clippy --all --benches --tests --examples --all-features

# Run tests
createdb ironclaw_test
cargo test

# Run specific test
cargo test test_name

OpenClaw Heritage

IronClaw is a Rust reimplementation inspired by OpenClaw. See FEATURE_PARITY.md for the complete tracking matrix.

Key differences:

  • Rust vs TypeScript - Native performance, memory safety, single binary
  • WASM sandbox vs Docker - Lightweight, capability-based security
  • PostgreSQL vs SQLite - Production-ready persistence
  • Security-first design - Multiple defense layers, credential protection

License

Licensed under either of:

at your option.

Description
IronClaw is OpenClaw inspired implementation in Rust focused on privacy and security IronClaw 基于一个简单的原则:你的 AI 助手应该为你服务,而不是与你为敌。
Readme 1.8 GiB
Languages
Rust 86%
Python 7.5%
JavaScript 3.4%
Shell 1.9%
CSS 0.9%
Other 0.2%