The suite asserted against whatever the developer's box happened to be
configured as. Two independent leaks, both surfaced by a real `~/.claw`:
1. `parse_args` resolves defaults — notably `permissions.defaultMode` —
through the user-scope config, which `default_config_home()` reads from
`CLAW_CONFIG_HOME` falling back to `$HOME/.claw`. Nothing overrode it, so
a `defaultMode` of `dontAsk` on the host turned 12 assertions from
`WorkspaceWrite` into `DangerFullAccess`. `env_lock()` now returns an
`EnvGuard` that points `CLAW_CONFIG_HOME` at a fresh empty directory and
restores the previous value on drop, so "no user config" is the state
under test. All 42 call sites already bind the guard, so this fixes them
at once; tests that want a populated config home can still set the var
themselves afterwards. `removed_login_and_logout_subcommands_error_helpfully`
asserted a Default-sourced permission mode without taking the guard at
all, and now does.
2. `direct_resume_safe_slash_commands_route_to_local_json_actions_831`
hardcoded `/sandbox` => "warn", which is only true where namespace
isolation is unsupported and just the filesystem sandbox is active
(#731). On a host with working user namespaces the correct answer is
"ok". That entry now accepts either; the test is about #831 routing —
local JSON action instead of `interactive_only` — not about the kernel
under the runner.
Verified with the real ~/.claw/settings.json in place (defaultMode=dontAsk):
cargo test --workspace 0 failures, clippy --workspace --all-targets
-D warnings 0 findings, scripts/fmt.sh clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9McEEH32ZtyM62GSXVSyJ
(cherry picked from commit 67aa4530c22f6fdf95a29d6a46992af137134c67)
`cargo clippy --workspace --all-targets -- -D warnings` is this repo's own
documented verification command, and it could not run to completion. Earlier
compile failures were masking a further 48 lints; with those cleared the
workspace now passes with zero findings.
Real fixes, not suppressions:
- status_json_value took 8 positional parameters, four of them a tail of
`Option<&_>` that most callers passed as None — trivially transposable.
Grouped into `StatusJsonExtras` with a Default impl; all 7 call sites now
name only the fields they actually set.
- extract_help_metadata's 8-tuple return is now a documented `HelpMetadata`
alias.
- the auto-compact loop indexed preserve_schedule by its loop counter;
it now iterates the array directly.
- a sandbox status chain had two identical arms; merged the condition.
- a filter_map whose every arm returned Some is now a map.
- 33 unused stdout/stderr bindings in the output-format contract tests are
prefixed with `_`, and 9 `map_or(false, ..)` are now `is_some_and(..)`.
NOTE: several `--bin claw` tests (parse_args defaults) read the *real*
`~/.claw/settings.json` rather than an isolated config home, so they fail on
any machine whose user config sets `permissions.defaultMode`. With that file
moved aside the binary's suite is 207/207. That isolation gap is pre-existing
and left for a separate change.
Verified: clippy workspace/all-targets/-D warnings reports 0; cargo test
--workspace passes with the user config neutralised; scripts/fmt.sh clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9McEEH32ZtyM62GSXVSyJ
(cherry picked from commit 03c8a8fd7ba6e405a8217d00d3766c1c6367e527)
`ApiError` is the `Err` half of nearly every `Result` in the api crate, and
its `Api` variant carried ~139 bytes inline (8 fields, four of them
`Option<String>`). That tripped clippy::result_large_err on 35 signatures
and, with -D warnings, made the whole crate unbuildable under the repo's
own documented verification command.
Move the payload into `ApiErrorDetails` behind a `Box`:
- `ApiError::Api(Box<ApiErrorDetails>)`, with `ApiError::api(..)` to
construct and `api_details()` to borrow
- `ApiErrorDetails` re-exported from the crate root
- adding a field here can no longer re-inflate every signature
This also simplifies `enrich_bearer_auth_error`, which previously
destructured and rebuilt all eight fields at five separate return points
just to append a hint; it now mutates the boxed payload in place.
Also clears the remaining pre-existing lints that blocked -D warnings:
unused test bindings, a redundant `use serde_json`, and an explicit
`.into_iter()` in claw-rag-service.
Verified: cargo test --workspace passes, 0 failures.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9McEEH32ZtyM62GSXVSyJ
(cherry picked from commit e9210bd75eb76e5d88b2ae797f537102bcabd41d)
An unbounded read_file returned the entire file, which on a small-context
backend overflows the model's window. The backend then rejects the whole
request (llama-server: "request (N tokens) exceeds the available context
size"), which kills the turn — so reading everything at once is precisely
what makes the session stop. Observed live: one 100 KB source file is
~25k tokens against a 32k window with a ~6k-token prefix, and a session
read the same file three times and died on six 400s.
read_file now returns a page and says so:
- absent `limit` yields DEFAULT_READ_LINES (2000) rather than EOF
- MAX_READ_CHARS (64 KB) bounds the payload even when `limit` is
explicit, so minified or generated files can't defeat the line cap
- `truncated` and `nextOffset` tell the caller it has a partial view
and exactly where to resume; the tool description explains both
MAX_READ_SIZE (10 MB) is untouched: it is a process guard against slurping
huge or binary blobs, not a context guard, and at ~2.5M tokens it never
fires on the files that actually overflow a window.
Also clears pre-existing clippy failures in trident.rs that blocked
-D warnings on every edit to this crate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9McEEH32ZtyM62GSXVSyJ
(cherry picked from commit c796d7e5eedc66b0cb37bb6823ec772f8ceef9cf)
refactor(cli): let the emitter report the output tail it leaves behind
Two places believed they owned terminal output: consume_stream streams
assistant deltas live while emit_output = true, and run_turn decided
whether to emit a separating newline before spinner.finish by guessing
from final_assistant_text(&summary).
That guess agrees with reality only because every *other* writer happens to
terminate its output with a newline — tool results go through
stream_markdown (which appends one), thinking summaries are written with a
trailing newline, and tool-call headers use writeln! — so the assistant
text path is the only one that can leave a partial line. That is a property
of the current renderers rather than of the streaming protocol, and it is
not enforced anywhere.
TerminalTail is state the emitter records into as it writes: both live
writers (AnthropicRuntimeClient::consume_stream and the tool-result blocks
in CliToolExecutor::execute) now push bytes through a TailTrackingWriter
that reports the last byte written, and the caller that ends a turn asks
where the emitter stopped instead of inferring it.
The heuristic also left the failure arm unguarded. A turn that streamed text
and then errored called spinner.fail on a partial line, which the line
clear then erased; that arm now carries the same guard.
Also gate the animated spinner.tick frame on is_terminal(). That frame is
pure cursor control (save, rewrite line, restore) and emits no newline, so
down a pipe it leaves an unterminated banner that the first streamed delta
then continues on the same physical line.
—
*[repo owner's gaebal-gajae (clawdbot) 🦞]*
- Updated SECURITY.md with explicit instructions for reporting vulnerabilities via GitHub Security Advisories
- Replaced 'when available' qualifier since Private Vulnerability Reporting is now enabled
- Provided step-by-step guidance for security researchers to submit vulnerability reports
Co-authored-by: clawdbot <clawdbot@ultraworkers.dev>
Reviewed and verified: 4 new tests added, all passing. 116 lines added to test JSON escape sequences and i64 boundaries. No production code changes.
—
*[repo owner's gaebal-gajae (clawdbot) 🦞]*
Add Asolaria json=0 wire-format + addressing example
Contributes a reproducible benchmark demonstrating:
- Binary wire format vs JSON text for agent event logs
- 4.64× size reduction (153.2 B → 33.0 B per record)
- ~11× faster encoding
- Tamper-detection via SHA-256 hash-chain
- Self-contained example with no changes to claw-code core
All measurements reproducible; architectural vision clearly distinguished.
Closes#3293
The api.star-history.com SVG endpoint is broken due to GitHub
stargazer API restrictions. Switch to the working alternative at
star-history.dera.page for both dark/light SVGs and the chart link.
Route groq-prefixed models through the OpenAI-compatible client,
apply Groq token-limit metadata, and skip local tool injection for
compound models.
Add regression coverage for routing, token limits, and compound
local-tool behavior.
Co-authored-by: Oz <oz-agent@warp.dev>
docs: add Simplified Chinese translation (README.zh.md)
Adds a complete Chinese translation of the README with proper language selector.
Contribution by @JasonYeYuhe
feat: add persistent auto-memory system
Implement a file-based persistent memory system that allows the AI to retain knowledge about users, feedback, projects, and references across sessions.
## Implementation Details
- MemoryStore data layer with FNV-1a workspace fingerprinting
- MemoryRead/MemoryWrite tools with security hardening (path traversal, symlink, absolute path, and size limit protections)
- System prompt injection of MEMORY.md index with prompt injection mitigation (fenced code block + trust-lowering notice)
- autoMemoryEnabled config option (defaults to true)
- Doctor health check for auto-memory state
- /memory command enhancement showing persistent memory info
## Verification
✓ 1023 lib tests pass
✓ Output format contract tests pass
✓ Release build succeeds
✓ Security hardening verified
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Asserts that an unquoted UNC path like \\server\share\file.txt survives
extract_path_candidates unchanged, preventing the regression where
shlex.split(posix=True) stripped the backslashes.
Replaces the basic extraction test with an end-to-end test that verifies
UNC paths are correctly denied when outside the workspace (e.g. local root),
but correctly allowed when the workspace root is itself a UNC share.
Validates the fix from the PR review.
Adds explicit test coverage to ensure drive-relative paths (e.g. C:foo)
and escaped UNC paths (e.g. \\\\server\\share\\dir) from JSON payloads
are correctly validated by the workspace containment checks.
Closes final review suggestions.
- Updates _check_bash_state to not short-circuit on the first unusable
bash candidate on Windows, ensuring a working fallback is found if present.
- Clears bash detection cache in unittest setUp/tearDown rather than just
relying on a pytest fixture, covering native unittest runners.
Adds a conftest.py pytest fixture to explicitly clear the lru_cache for bash
detection, addressing the monkeypatching hazard in a way that doesn't rely
solely on comments. (Note: CI currently runs python -m unittest, but this
keeps local pytest runners safe.)
Also updates the bash check to run the execution probe directly inside the
cached _check_bash_state, exposing the difference between 'no bash found'
and 'bash present but execution failed' directly to the unittest skip reason.
Moves get_bash_executable and require_bash into a shared module so their
lru_cache state is per-session rather than per-test-module. Also adds a
comment regarding monkeypatching since lru_cache will silently drop changes
to os.environ if it has already been populated.
Closes follow-up items from PR review.
- Fixed an issue in extract_path_candidates where shlex.split(posix=True)
would strip backslashes from Windows paths, mangling UNC paths (e.g. \\server\share)
before they could be evaluated by _is_windows_absolute.
- Fixed a bypass in validate_path where Windows absolute paths bypassed glob
expansion and symlink resolution. On Windows, they now fall through to the
standard Path logic, allowing glob expansion and strict resolution while still
properly checking containment.
* fix(runtime): prompt permission mode never invokes the prompter
PermissionMode derives Ord from declaration order, placing Prompt (and Allow)
above the real privilege ladder (ReadOnly < WorkspaceWrite < DangerFullAccess).
In PermissionPolicy::authorize_with_context the ladder check
'current_mode >= required_mode' therefore evaluates true for every tool when the
active mode is Prompt (3 >= {0,1,2}), returning Allow and making the subsequent
'if current_mode == PermissionMode::Prompt' branch that routes to prompt_or_deny
unreachable.
Effect: a session configured with the 'prompt' permission mode silently
auto-allows every tool and never calls the prompter (reached via
ConversationRuntime::run_turn with an interactive prompter), defeating the mode's
entire purpose. The sibling PermissionEnforcer already special-cases Prompt
before its own 'active_mode >= required_mode' check, confirming Prompt is not
meant to be part of the ordered ladder.
Fix: exclude Prompt from the ordered-ladder comparison so Prompt mode falls
through to the interactive prompt path. Allow remains handled by its explicit
equality check. The same comparison appears twice in authorize_with_context —
once in the shared tail and once inside the 'Some(PermissionOverride::Allow)'
arm, which is reached when a hook emits permissionDecision "allow" — so both
copies carry the guard; guarding only the tail still left a hook able to
auto-allow every tool in Prompt mode without ever consulting the prompter.
Adds regression tests covering Prompt mode with a prompter, without a prompter,
and with a hook allow override.
* refactor(runtime): make the permission privilege ladder an explicit rank
`PermissionMode` derived `PartialOrd`/`Ord`, so the ladder deciding whether a
tool runs without asking was declaration order. A variant inserted in the middle
of the enum re-ranked it silently, and the difference between "this tool may
run" and "this tool may run after a prompt" was a property of where an `enum`
line happened to sit. That is how `Prompt` came to outrank `DangerFullAccess`,
satisfy `current_mode >= required_mode` for every tool, and skip the prompter.
Replace the derive with `privilege_rank`, an exhaustive `match` naming every
variant, so inserting one is a build failure rather than a silent re-ranking.
`Ord` is deliberately not derived, so a stray `>=` on two modes no longer
compiles; the two comparisons outside this module now say which rank they read
(`PermissionEnforcer::check_with_required_mode` and the `claw doctor` permission
check). Both keep their previous answers, since `privilege_rank` reproduces the
old declaration order exactly, including `Prompt` and `Allow` above the tiers.
`Prompt` and `Allow` are modes of operation, not privilege tiers, so they are
never ranked *against* a tool requirement. The new `covers` predicate states
that by name rather than by number: `Allow` always covers, `Prompt` never does
(it asks), and only the three tiers compare ranks.
That also collapses the duplicated ladder in `authorize_with_context`. The
`Some(PermissionOverride::Allow)` arm carried a second, identical copy of the
ask-rule check and the ladder; it now falls through to the single copy below,
so a hook `allow` is an override rather than a bypass and the two copies can no
longer drift apart.
Behaviour is unchanged for every (mode, requirement, rule, override)
combination. The tests pin the `Prompt` routes — plain and hook-`allow` — across
all three tiers, plus the rank ordering itself.
docs: correct rust workspace crate count to 11
This updates the documentation to reflect the actual number of crates in the
rust workspace, which is 11, not 9. The workspace contains:
- api
- claw-analog
- claw-rag-service
- commands
- compat-harness
- mock-anthropic-service
- plugins
- runtime
- rusty-claude-cli
- telemetry
- tools
load_session/save_session built the session file path directly from the
caller-supplied session_id with no validation, so a value like
'../../../tmp/secrets' (reachable from the 'load-session' CLI subcommand)
could read or write JSON files outside the session directory.
Add _validate_session_id() to reject anything outside [A-Za-z0-9_-]
(path separators, '..', absolute/Windows paths) before the id is used to
build a filename. Legitimate ids (uuid4 hex, 'session-<digits>-<n>') are
unaffected.
Adds SessionStorePathTraversalTests covering traversal rejection on both
load and save plus a legitimate-id round-trip.