Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rust/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🦞 Claw Code — Rust Implementation

A high-performance Rust rewrite of the Claude Code CLI agent harness. Built for speed, safety, and native tool execution.
A high-performance Rust rewrite of the Claw Code CLI agent harness. Built for speed, safety, and native tool execution.

## Quick Start

Expand Down
6 changes: 3 additions & 3 deletions rust/crates/compat-harness/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,16 @@ fn upstream_repo_candidates(primary_repo_root: &Path) -> Vec<PathBuf> {
}

for ancestor in primary_repo_root.ancestors().take(4) {
candidates.push(ancestor.join("claude-code"));
candidates.push(ancestor.join("claw-code"));
candidates.push(ancestor.join("clawd-code"));
}

candidates.push(
primary_repo_root
.join("reference-source")
.join("claude-code"),
.join("claw-code"),
);
candidates.push(primary_repo_root.join("vendor").join("claude-code"));
candidates.push(primary_repo_root.join("vendor").join("claw-code"));

let mut deduped = Vec::new();
for candidate in candidates {
Expand Down
104 changes: 91 additions & 13 deletions rust/crates/runtime/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,20 @@ pub struct RuntimeConfig {

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RuntimeFeatureConfig {
hooks: RuntimeHookConfig,
mcp: McpConfigCollection,
oauth: Option<OAuthConfig>,
model: Option<String>,
permission_mode: Option<ResolvedPermissionMode>,
sandbox: SandboxConfig,
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RuntimeHookConfig {
pre_tool_use: Vec<String>,
post_tool_use: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct McpConfigCollection {
servers: BTreeMap<String, ScopedMcpServerConfig>,
Expand Down Expand Up @@ -221,6 +228,7 @@ impl ConfigLoader {
let merged_value = JsonValue::Object(merged.clone());

let feature_config = RuntimeFeatureConfig {
hooks: parse_optional_hooks_config(&merged_value)?,
mcp: McpConfigCollection {
servers: mcp_servers,
},
Expand Down Expand Up @@ -278,6 +286,11 @@ impl RuntimeConfig {
&self.feature_config.mcp
}

#[must_use]
pub fn hooks(&self) -> &RuntimeHookConfig {
&self.feature_config.hooks
}

#[must_use]
pub fn oauth(&self) -> Option<&OAuthConfig> {
self.feature_config.oauth.as_ref()
Expand All @@ -300,6 +313,17 @@ impl RuntimeConfig {
}

impl RuntimeFeatureConfig {
#[must_use]
pub fn with_hooks(mut self, hooks: RuntimeHookConfig) -> Self {
self.hooks = hooks;
self
}

#[must_use]
pub fn hooks(&self) -> &RuntimeHookConfig {
&self.hooks
}

#[must_use]
pub fn mcp(&self) -> &McpConfigCollection {
&self.mcp
Expand All @@ -326,6 +350,26 @@ impl RuntimeFeatureConfig {
}
}

impl RuntimeHookConfig {
#[must_use]
pub fn new(pre_tool_use: Vec<String>, post_tool_use: Vec<String>) -> Self {
Self {
pre_tool_use,
post_tool_use,
}
}

#[must_use]
pub fn pre_tool_use(&self) -> &[String] {
&self.pre_tool_use
}

#[must_use]
pub fn post_tool_use(&self) -> &[String] {
&self.post_tool_use
}
}

impl McpConfigCollection {
#[must_use]
pub fn servers(&self) -> &BTreeMap<String, ScopedMcpServerConfig> {
Expand Down Expand Up @@ -424,6 +468,22 @@ fn parse_optional_model(root: &JsonValue) -> Option<String> {
.map(ToOwned::to_owned)
}

fn parse_optional_hooks_config(root: &JsonValue) -> Result<RuntimeHookConfig, ConfigError> {
let Some(object) = root.as_object() else {
return Ok(RuntimeHookConfig::default());
};
let Some(hooks_value) = object.get("hooks") else {
return Ok(RuntimeHookConfig::default());
};
let hooks = expect_object(hooks_value, "merged settings.hooks")?;
Ok(RuntimeHookConfig {
pre_tool_use: optional_string_array(hooks, "PreToolUse", "merged settings.hooks")?
.unwrap_or_default(),
post_tool_use: optional_string_array(hooks, "PostToolUse", "merged settings.hooks")?
.unwrap_or_default(),
})
}

fn parse_optional_permission_mode(
root: &JsonValue,
) -> Result<Option<ResolvedPermissionMode>, ConfigError> {
Expand Down Expand Up @@ -659,26 +719,42 @@ fn optional_u16(
fn optional_string_array(
object: &BTreeMap<String, JsonValue>,
key: &str,
context: &str,
_context: &str,
) -> Result<Option<Vec<String>>, ConfigError> {
match object.get(key) {
Some(value) => {
let Some(array) = value.as_array() else {
return Err(ConfigError::Parse(format!(
"{context}: field {key} must be an array"
)));
// Not an array — treat as absent (e.g. hooks in object format).
return Ok(None);
};
array
let strings: Vec<String> = array
.iter()
.map(|item| {
item.as_str().map(ToOwned::to_owned).ok_or_else(|| {
ConfigError::Parse(format!(
"{context}: field {key} must contain only strings"
))
})
.filter_map(|item| {
// Accept plain strings directly.
if let Some(s) = item.as_str() {
return Some(s.to_owned());
}
// Accept object-style hook entries: extract nested command strings.
if let Some(obj) = item.as_object() {
if let Some(hooks_arr) = obj.get("hooks").and_then(JsonValue::as_array) {
for hook in hooks_arr {
if let Some(cmd) =
hook.as_object().and_then(|h| h.get("command")).and_then(JsonValue::as_str)
{
return Some(cmd.to_owned());
}
}
}
}
// Skip unrecognized entries gracefully.
None
})
.collect::<Result<Vec<_>, _>>()
.map(Some)
.collect();
if strings.is_empty() {
Ok(None)
} else {
Ok(Some(strings))
}
}
None => Ok(None),
}
Expand Down Expand Up @@ -836,6 +912,8 @@ mod tests {
.and_then(JsonValue::as_object)
.expect("hooks object")
.contains_key("PostToolUse"));
assert_eq!(loaded.hooks().pre_tool_use(), &["base".to_string()]);
assert_eq!(loaded.hooks().post_tool_use(), &["project".to_string()]);
assert!(loaded.mcp().get("home").is_some());
assert!(loaded.mcp().get("project").is_some());

Expand Down
Loading