feat(install.sh): build + register zeroclawed-mcp with Claude Code (#32)#26
feat(install.sh): build + register zeroclawed-mcp with Claude Code (#32)#26
Conversation
Adds `!secure` as a post-auth chat command dispatched before any agent
sees the message. Subcommands:
- `!secure set NAME=value` — stores the secret in fnox and replies
with "Stored `NAME`" + a retention-warning. Never echoes the value.
- `!secure list` — lists stored secret names (not values). Parses
fnox's output defensively; extra columns that look like values are
stripped before replying.
- `!secure help` / `!secure` — usage string.
Dispatch model (matches existing `!switch`/`!sessions`/`!default`):
- `CommandHandler::is_secure_command(&str)` — static, case-insensitive.
- `CommandHandler::handle_secure(text, identity_id) -> String` —
async (shells out to fnox), takes identity for future per-identity
audit/ACL though not used yet. Wired into telegram.rs in both
`handle_message_nonblocking` and `handle_message` paths, and added
to the unknown-command exclusion list so `!secure` doesn't get
intercepted as unknown before hitting the post-auth handler.
Subprocess approach today — `Command::new("fnox").args(["set", …])`.
Acknowledged-cost: three places now shell out to fnox (vault.rs,
secure_set, secure_list), which means three places that need an
availability check. **Follow-up PR (in flight) will migrate all three
to the fnox library crate via `fnox = { git = "https://github.com/jdx/fnox" }`**
— fnox already exposes public modules (secret_resolver, commands,
etc.) at `src/lib.rs`, so no upstream PR is needed.
Redaction discipline:
- The success reply names the stored secret but never echoes the
value; tested.
- The failure reply surfaces fnox's stderr (user needs to know why
it failed) but doesn't carry the value; tested.
- The telegram channel's `debug!` logs only that a !secure command
was handled, without the message text. A `!secure set FOO=bar`
would otherwise leak the value into ops logs.
- Name validation: `[A-Za-z0-9_-]+` only (matches the substitution
engine's accepted ref-name syntax — so a stored secret can be
immediately referenced as `{{secret:NAME}}` without a rename).
Tested with three invalid-char cases.
5 given/when/then behavioral tests:
- `secure_set_reply_includes_name_but_not_value`
- `secure_set_surfaces_fnox_error_without_echoing_value`
- `secure_unknown_subcommand_returns_help`
- `secure_set_rejects_invalid_name_chars`
- `secure_list_returns_names_only`
All use a fake-fnox shell script installed on PATH via TempDir, so
the tests are hermetic regardless of whether the real fnox is
installed on the dev/CI machine.
Explicit non-goals for this PR (follow-ups):
- `!secure remove NAME` — pending a decision on whether fnox's
own remove command or a config-file edit is the right approach.
- `!secure request NAME` — the out-of-band localhost-paste flow
that avoids chat-transport retention entirely (see
docs/rfcs/agent-secret-gateway.md §5).
- Matrix/WhatsApp channel wiring — same pattern as telegram, but
adds separate verification surface. Doing in a follow-up to keep
this PR reviewable.
- Library-based fnox integration (swap subprocess → lib calls) —
committed as next PR per user direction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User and I had a back-and-forth on whether to migrate fnox usage to
the upstream Rust library or keep subprocess. After investigating
fnox's actual library API (`fnox::secret_resolver::resolve_secret`
needs pre-loaded `Config` + per-secret `SecretConfig`; no clean
programmatic SET; ~30 transitive deps), subprocess is still the
right tool — but each call-site shouldn't be reinventing the wheel.
This commit lands `crates/onecli-client/src/fnox_client.rs` — one
typed wrapper around `fnox` that all current and future call-sites
use:
- `FnoxClient::get(name) -> Result<String, FnoxError>`
- `FnoxClient::set(name, value) -> Result<(), FnoxError>`
- `FnoxClient::list() -> Result<Vec<String>, FnoxError>`
- `FnoxClient::is_available() -> bool`
Errors are typed (`NotInstalled`, `EmptyValue`, `Failed { exit_code,
stderr }`, `InvalidUtf8`), not stringified-stderr, so callers can
pattern-match: e.g. `!secure set` gives a different message on
NotInstalled (suggest `brew install fnox`) vs Failed (surface fnox's
own error).
Migration in this PR:
- `commands.rs::secure_set` and `secure_list` now call FnoxClient.
Before: each had its own `Command::new("fnox")`, its own io::Error
-> "not available" stringification, its own stderr-to-string error.
After: one-liner calls + match on the typed error.
Migration left for follow-up (when PR #15 lands or this branch is
rebased on it):
- `vault.rs::get_secret_from_fnox` — same swap; needs PR #15 in
history first since that's where the function lives.
Tests:
- 11 new given/when/then tests for FnoxClient itself, using
`FnoxClient::with_binary(path)` to point at fake-fnox shell scripts
in TempDir. No PATH manipulation, no global env mutation, no
`serial_test` requirement (cleaner than the existing fake-fnox-on-
PATH pattern in `vault_fallthrough.rs` and `commands::tests`).
- Existing 5 `!secure` tests in `commands::tests` still pass
unchanged — they used PATH-prepend, which still works because
`FnoxClient::new()` defaults to `"fnox"` (PATH lookup).
- Notable adversarial assertions added:
- `get_empty_value_is_error_not_empty_string` — empty stdout
returned as `EmptyValue(name)` so a downstream `Authorization:
Bearer ` (empty) silent-anonymous failure is impossible.
- `set_passes_name_and_value_in_argv_order` — captures argv to a
temp file and asserts position; guards against an off-by-one
rearrangement that would store the value under the wrong name.
- `list_extracts_names_only_dropping_value_columns` — explicit
negative assertion that no value substring survives.
`tempfile` added as a dev-dep on `onecli-client`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per docs/rfcs/agent-secret-gateway.md §4. New crate
\`crates/zeroclawed-mcp\` exposes a deliberately narrow MCP surface
that lets agents:
- discover what secrets exist (\`list_secrets\` returns NAMES only,
never values)
- build canonical references for substitution
(\`secret_reference(name) → "{{secret:NAME}}"\`)
- initiate user-facing add flows
(\`add_secret_request(name, description, retention_ok)\` — currently
stub, future will return a short-lived localhost URL)
**Critically, the server does NOT expose \`get_secret\`.** That's the
core threat-model decision: any agent that connects to MCP could
otherwise enumerate names and pull values. Values flow through the
security-proxy substitution layer ONLY, where they're injected at the
network boundary and never enter agent context.
## Implementation
Built on \`rmcp\` v0.8 (the official Rust MCP SDK) with the
\`server + macros + transport-io + schemars\` features. \`#[tool_router]\`
+ \`#[tool_handler]\` attributes wire the three tools through stdio
JSON-RPC. \`schemars\` v1 (matching rmcp's transitive requirement —
catching this misalignment cost a build cycle).
Wraps \`onecli_client::FnoxClient\` for the underlying secret store.
Tests inject a fake-\`fnox\` shell script via
\`FnoxClient::with_binary(path)\` so they're hermetic — no PATH
manipulation, no env mutation.
## Why a separate binary, not a module on \`zeroclawed\`
MCP servers run as agent-spawned subprocesses over stdio. Splitting
this out:
1. Agents that don't need secret discovery don't pay the startup cost.
2. Server can be installed by an agent that doesn't have access to
the full zeroclawed daemon (e.g., \`claude\` in a sandbox).
3. Failures here don't take down the gateway.
## Validation symmetry
\`is_valid_name\` (in this crate) matches \`substitution::find_refs\`
name-shape rules in security-proxy exactly: \`[A-Za-z0-9_-]+\`. Tests
guard the symmetry — if MCP returned a token the substitution engine
wouldn't accept, the agent's request would silently fail downstream.
## Tests (7 given/when/then)
- \`list_secrets_returns_name_list_with_count\` — happy path with
3-name fixture
- \`list_secrets_returns_actionable_error_when_fnox_missing\` — error
path returns "install fnox" hint, not a stack trace
- \`secret_reference_returns_canonical_token\` — exact token shape +
network-boundary usage hint
- \`secret_reference_rejects_invalid_name_shape\` — name validation
prevents downstream silent failures
- \`add_secret_request_with_retention_ok_offers_chat_path\` — both
routes (host fnox + !secure set) suggested
- \`add_secret_request_with_retention_not_ok_rejects_chat_path\` —
high-value secrets must NOT slip through to chat just because the
agent forgot to set the flag (safety contract)
- \`server_advertises_tools_capability\` — guards against macro
registration silently losing a tool
## Stacks on PR #21
Depends on \`onecli_client::FnoxClient\` from PR #21 for the
\`fnox.list()\` call. PR #21 must merge first or this rebases.
## Follow-ups
- Wire the server into agent configs (#32 — install.sh seeds the
\`mcp_servers\` entry for claude/opencode/etc.)
- Implement the \`!secure request\` localhost-paste flow that
\`add_secret_request\` currently stubs out
- Add a \`describe_secret(name)\` tool returning rich metadata
(description, last-rotated, allowed-destinations) without ever
returning the value
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the MCP server scaffolded in PR #23 into Claude Code via \`~/.claude/settings.json\`'s \`mcpServers\` section. Now the agent auto-discovers \`list_secrets\`, \`secret_reference\`, and \`add_secret_request\` on every Claude Code session start — no manual config required. ## Two changes to install.sh 1. **Build + install the binary**: section 1's release-build step now includes \`-p zeroclawed-mcp\`, and the install loop copies \`zeroclawed-mcp\` into \`$BIN_DIR\` alongside \`clashd\`, \`zeroclawed\`, and \`security-proxy\`. 2. **Register with Claude Code** (section 4 / Claude Code hook): a second python block writes \`mcpServers["zeroclawed-secrets"]\` with the absolute path to the installed binary. Idempotent — re-running install.sh replaces the entry instead of duplicating. Smoke-tested locally with both first-time and second-time runs: first run creates the entry, second run leaves it identical (deterministic JSON output). If the binary isn't found at \`$BIN_DIR/zeroclawed-mcp\` (e.g. the build step was skipped via \`--configure-only\`), install.sh prints a warn-and-skip with a build hint instead of failing — lets operators run \`--configure-only\` against an already-built tree. ## Follow-up - opencode and openclaw MCP registration TBD when those agents' config schemas are nailed down. Same idempotent-python pattern will extend cleanly. - A \`describe_secret(name)\` tool exposing per-secret metadata (description, allowlist, last-rotated) would make discovery meaningful even on fresh deployments — currently \`list_secrets\` returns names only. ## Stacks on PR #23 Depends on the \`zeroclawed-mcp\` crate from PR #23. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR wires the zeroclawed-mcp binary into the installer so Claude Code can auto-register the MCP server in ~/.claude/settings.json, and adds supporting secret-management plumbing (FnoxClient, !secure command path) to avoid routing secret values through agent context.
Changes:
- Extend
scripts/install.shto build/installzeroclawed-mcpand register it undermcpServers.zeroclawed-secretsin Claude Code settings. - Add a new
zeroclawed-mcpcrate (stdio MCP server) exposinglist_secrets,secret_reference, andadd_secret_request. - Introduce
onecli_client::FnoxClientwrapper and add!securecommand handling (Telegram + command layer) for set/list without agent exposure.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/install.sh | Builds/installs zeroclawed-mcp and mutates Claude Code settings.json to register it as an MCP server |
| crates/zeroclawed/src/commands.rs | Adds !secure parsing/handlers and related tests using a fake fnox |
| crates/zeroclawed/src/channels/telegram.rs | Intercepts !secure post-auth to avoid routing to agents and avoids logging the raw command text |
| crates/zeroclawed-mcp/src/main.rs | Binary entrypoint wiring MCP server to rmcp stdio transport with tracing to stderr |
| crates/zeroclawed-mcp/src/lib.rs | Implements MCP tools (list_secrets, secret_reference, add_secret_request) over FnoxClient |
| crates/zeroclawed-mcp/Cargo.toml | Declares the new crate + dependencies |
| crates/onecli-client/src/lib.rs | Exposes fnox_client module + re-exports FnoxClient/FnoxError |
| crates/onecli-client/src/fnox_client.rs | Adds typed wrapper around the fnox CLI (get/set/list/is_available) + tests |
| crates/onecli-client/Cargo.toml | Adds tempfile dev-dependency for new tests |
| Cargo.toml | Adds crates/zeroclawed-mcp to workspace members and default-members |
| Cargo.lock | Locks new dependencies (rmcp, rmcp-macros, etc.) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Crate-specific | ||
| # Official Rust MCP SDK. The "server" feature pulls in the | ||
| # stdio transport and tool/resource registration macros. | ||
| rmcp = { version = "0.8", features = ["server", "macros", "transport-io", "schemars"] } |
| /// `fnox set <name> <value>` — store a secret. | ||
| /// | ||
| /// `value` is passed as a CLI argument, which fnox docs note is | ||
| /// visible in `ps` output during the call. For our use case | ||
| /// (`!secure set`) the value is already in the chat transport, | ||
| /// so this isn't a regression. Future improvement: pipe value | ||
| /// via stdin, which fnox supports when value is omitted. | ||
| pub async fn set(&self, name: &str, value: &str) -> Result<(), FnoxError> { | ||
| debug!("fnox set {}", name); | ||
| let output = self | ||
| .run(&["set", name, value]) | ||
| .await | ||
| .map_err(FnoxError::NotInstalled)?; |
| /// `fnox list` — return the list of stored secret NAMES. | ||
| /// | ||
| /// Defensive parse: fnox's CLI output format varies slightly by | ||
| /// version (some versions emit a table, some emit `name value` | ||
| /// pairs). We extract the first whitespace-separated token from | ||
| /// each non-comment, non-empty line and treat it as a name. | ||
| /// Anything else on the line — values, descriptions, table | ||
| /// borders — is dropped. Callers that need richer info should | ||
| /// use `fnox list --output json` directly once we wire that up. | ||
| pub async fn list(&self) -> Result<Vec<String>, FnoxError> { | ||
| debug!("fnox list"); | ||
| let output = self.run(&["list"]).await.map_err(FnoxError::NotInstalled)?; | ||
|
|
||
| if !output.status.success() { | ||
| return Err(FnoxError::Failed { | ||
| exit_code: output.status.code(), | ||
| stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(), | ||
| }); | ||
| } | ||
|
|
||
| let stdout = String::from_utf8_lossy(&output.stdout); | ||
| let names = stdout | ||
| .lines() | ||
| .filter_map(|line| { | ||
| let trimmed = line.trim(); | ||
| if trimmed.is_empty() || trimmed.starts_with('#') { | ||
| None | ||
| } else { | ||
| trimmed.split_whitespace().next().map(String::from) | ||
| } | ||
| }) | ||
| .collect(); | ||
| Ok(names) | ||
| } |
| /// Handle `!secure <subcommand>`. Dispatches to `fnox` as a | ||
| /// subprocess; nothing about the secret value transits an agent's | ||
| /// context. Never echoes back values — responses are name-only. | ||
| /// | ||
| /// Subcommands: | ||
| /// - `!secure set NAME=value` — store a secret | ||
| /// - `!secure list` — list stored secret names | ||
| /// - `!secure help` — usage string | ||
| /// | ||
| /// **Retention warning** (documented in first-time-use UX): | ||
| /// this command's text passes through the chat transport | ||
| /// (Telegram/Matrix/WhatsApp), which retains message history. | ||
| /// For values where chat-transport exposure is unacceptable, | ||
| /// a follow-up `!secure request NAME` flow (out-of-band) is | ||
| /// planned — see `docs/rfcs/agent-secret-gateway.md` §5. | ||
| pub async fn handle_secure(&self, text: &str, _identity_id: &str) -> String { | ||
| let trimmed = text.trim(); | ||
| // `!secure ...` — split off the subcommand word. | ||
| let mut parts = trimmed.splitn(3, ' '); | ||
| let _lead = parts.next(); // `!secure` | ||
| let sub = parts.next().map(|s| s.to_lowercase()).unwrap_or_default(); | ||
| let rest = parts.next().unwrap_or("").trim(); | ||
|
|
||
| match sub.as_str() { | ||
| "set" => secure_set(rest).await, | ||
| "list" => secure_list().await, | ||
| "help" | "" => secure_help(), | ||
| _ => format!( | ||
| "⚠️ Unknown !secure subcommand: `{sub}`\n\n{}", | ||
| secure_help() | ||
| ), | ||
| } | ||
| } |
| // `!secure ...` — split off the subcommand word. | ||
| let mut parts = trimmed.splitn(3, ' '); | ||
| let _lead = parts.next(); // `!secure` |
| (n.trim().to_string(), v[1..].to_string()) | ||
| } | ||
| None => { | ||
| let mut parts = rest.splitn(2, ' '); | ||
| let n = parts.next().unwrap_or("").trim().to_string(); | ||
| let v = parts.next().unwrap_or("").to_string(); |
| // Only allow the same name-shape substitution accepts, so callers | ||
| // can immediately use the stored name in a `{{secret:NAME}}` ref. | ||
| // See crates/security-proxy/src/substitution.rs. | ||
| if !name | ||
| .bytes() | ||
| .all(|c| c.is_ascii_alphanumeric() || c == b'_' || c == b'-') | ||
| { |
| # Official Rust MCP SDK. The "server" feature pulls in the | ||
| # stdio transport and tool/resource registration macros. | ||
| rmcp = { version = "0.8", features = ["server", "macros", "transport-io", "schemars"] } | ||
| schemars = "1" |
| // Safety: tests holding SECURE_ENV_MUTEX serialize env | ||
| // mutation. `std::env::set_var` is marked unsafe in | ||
| // Rust 2024 for this exact reason. | ||
| unsafe { | ||
| std::env::set_var("PATH", new_path); | ||
| } | ||
| Self { original } | ||
| } | ||
| } | ||
| impl Drop for PathGuard { | ||
| fn drop(&mut self) { | ||
| unsafe { | ||
| match &self.original { | ||
| Some(p) => std::env::set_var("PATH", p), | ||
| None => std::env::remove_var("PATH"), | ||
| } | ||
| } |
| /// Validates a secret name against the same shape the substitution | ||
| /// engine accepts (see `crates/security-proxy/src/substitution.rs`). | ||
| /// Keeping the two in sync is critical: if `secret_reference` | ||
| /// produced a token the substitution engine wouldn't accept, the | ||
| /// agent's request would silently fail downstream. | ||
| fn is_valid_name(name: &str) -> bool { |
|
Codex integration sweep note: I reviewed the inline comments on this PR. GitHub rejected direct inline replies for these older/outdated review comments with HTTP 422, so I am responding top-level instead: 3141273068, 3141273080, 3141273087, 3141273094, 3141273098, 3141273100, 3141273105, 3141273116, 3141273122, 3141273130.\n\nI did not edit this branch. Items that overlap the secure/fnox/host-agent/digest integration work are addressed in draft PR #38 (codex-integration-code), including stdin-based fnox set, bounded fnox waits, whitespace-safe !secure parsing, identity-aware !secure audit logs, valid-input host-agent properties, real WhatsApp HMAC verification, loopback OneCLI default bind, and race-free digest temp paths. Remaining PR-specific findings stay actionable for this branch owner or a follow-up. |
Cross-cutting triage finding (PRs #20, #21, #23, #26, #28, #31): the !secure parser used `splitn(' ')` which mis-shapes multi-space input — `"!secure set NAME=v"` parsed as sub="" rest="set NAME=v", silently routing through the unknown-subcommand path with the secret in the help-error message. Switch to `split_whitespace()`. Also wires the previously-unused `identity_id` into an audit log so the `_` prefix can drop. The log records identity + subcommand, never the value (`rest`). Closes the "audit-claim docstring with no audit implementation" finding from triage.
…ning (#44) Squash-merge of integration/super-combined — 4 weeks of feature work + cross-PR security fixes + codex agent's hardening, all green CI (14/14 checks). ## Features landing - **fnox secret-resolver integration** (#15) + FnoxClient subprocess wrapper (#21) - **Adversarial commit-reviewer + mechanical pre-commit gate** (#18) - **{{secret:NAME}} substitution engine** in security-proxy URL/headers/body (#19) - **Per-secret destination allowlist** (#22) — RFC §11.1 attack defense - **!secure chat commands** (set/list) on Telegram (#20), Matrix (#28), WhatsApp (#31) - **zeroclawed-mcp** scaffold — agent-facing secret discovery server (#23) - **install.sh wires MCP** into Claude Code agent configs (#26) - **zeroclawed-secret-paste** — localhost web UI for one-shot secret input (#34) - **Bulk paste UI** — .env-style multi-secret onboarding with per-line results - **LAN-friendly defaults** — bind 0.0.0.0 + RFC 1918 Origin acceptance - **WhatsApp HMAC verification** (was always-true placeholder before — codex hardening) ## Security fixes folded in - /vault/:secret bearer auth + 127.0.0.1 default bind (#39) - URL-embedded secrets honor destination allowlist (#41) - Paste-flow: bearer URL only at debug, fnox set via stdin not argv (#40) - Paste-flow: graceful shutdown, exit-on-submit, reject Origin: null (#43) - Subprocess timeouts + kill_on_drop on FnoxClient - BrokenPipe-tolerant stdin write (Linux CI surface) - Header-value log redaction - OneCLI bound to 127.0.0.1 by default - Sanitized real API token + Telegram IDs from sample configs (#36) ## Architecture / refactors - Consolidated onecli binary into security-proxy (#17) - Hardcoded vault URL removed from onecli-client - security-proxy resolver wired into hot path - Extracted build_app router; migrated /vault/:secret route - !secure parser uses split_whitespace (was splitn), audit-logs invocations ## Test coverage added - security-proxy substitution engine + body/headers tests - onecli-client retry + Http(_) variant + adversarial fallthrough suite - onecli-client client.rs rewritten from tautologies to wiremock-backed - config/validator coverage (was zero, now 290-line module covered) - 16 zeroclawed-secret-paste tests including bulk-mode cases ## Docs / RFCs - agent-secret-gateway holistic architecture - consolidation-findings (what #28 must address) - secret-input-web-ui RFC (input-only, new-by-default) - browser-harness integration spike - test-quality-audit Round 1+2+3 (host-agent + zeroclawed priority files) ## Codex agent's hardening cherry-picks - Subprocess timeouts on fnox calls - map_spawn_error helper - Validator hardening + atomic-counter digest race fix - WhatsApp HMAC implementation + tests - proxy header-value log redaction CI: all 14 checks green at squash time. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
Subsumed by #44 (squashed to |
…ning (#44) Squash-merge of integration/super-combined — 4 weeks of feature work + cross-PR security fixes + codex agent's hardening, all green CI (14/14 checks). ## Features landing - **fnox secret-resolver integration** (#15) + FnoxClient subprocess wrapper (#21) - **Adversarial commit-reviewer + mechanical pre-commit gate** (#18) - **{{secret:NAME}} substitution engine** in security-proxy URL/headers/body (#19) - **Per-secret destination allowlist** (#22) — RFC §11.1 attack defense - **!secure chat commands** (set/list) on Telegram (#20), Matrix (#28), WhatsApp (#31) - **zeroclawed-mcp** scaffold — agent-facing secret discovery server (#23) - **install.sh wires MCP** into Claude Code agent configs (#26) - **zeroclawed-secret-paste** — localhost web UI for one-shot secret input (#34) - **Bulk paste UI** — .env-style multi-secret onboarding with per-line results - **LAN-friendly defaults** — bind 0.0.0.0 + RFC 1918 Origin acceptance - **WhatsApp HMAC verification** (was always-true placeholder before — codex hardening) ## Security fixes folded in - /vault/:secret bearer auth + 127.0.0.1 default bind (#39) - URL-embedded secrets honor destination allowlist (#41) - Paste-flow: bearer URL only at debug, fnox set via stdin not argv (#40) - Paste-flow: graceful shutdown, exit-on-submit, reject Origin: null (#43) - Subprocess timeouts + kill_on_drop on FnoxClient - BrokenPipe-tolerant stdin write (Linux CI surface) - Header-value log redaction - OneCLI bound to 127.0.0.1 by default - Sanitized real API token + Telegram IDs from sample configs (#36) ## Architecture / refactors - Consolidated onecli binary into security-proxy (#17) - Hardcoded vault URL removed from onecli-client - security-proxy resolver wired into hot path - Extracted build_app router; migrated /vault/:secret route - !secure parser uses split_whitespace (was splitn), audit-logs invocations ## Test coverage added - security-proxy substitution engine + body/headers tests - onecli-client retry + Http(_) variant + adversarial fallthrough suite - onecli-client client.rs rewritten from tautologies to wiremock-backed - config/validator coverage (was zero, now 290-line module covered) - 16 zeroclawed-secret-paste tests including bulk-mode cases ## Docs / RFCs - agent-secret-gateway holistic architecture - consolidation-findings (what #28 must address) - secret-input-web-ui RFC (input-only, new-by-default) - browser-harness integration spike - test-quality-audit Round 1+2+3 (host-agent + zeroclawed priority files) ## Codex agent's hardening cherry-picks - Subprocess timeouts on fnox calls - map_spawn_error helper - Validator hardening + atomic-counter digest race fix - WhatsApp HMAC implementation + tests - proxy header-value log redaction CI: all 14 checks green at squash time. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Summary
Wires the MCP server scaffolded in PR #23 into Claude Code via
`~/.claude/settings.json`'s `mcpServers`. Agent auto-discovers
`list_secrets`, `secret_reference`, `add_secret_request` on
every Claude Code session — no manual config required.
Changes
`-p zeroclawed-mcp`; install loop copies it into `$BIN_DIR`
alongside the other binaries.
`mcpServers["zeroclawed-secrets"]` with the absolute binary
path. Idempotent (smoke-tested with two runs producing
identical JSON).
If the binary isn't built (e.g. `--configure-only` mode), prints a
warn-and-skip with a build hint instead of failing.
Follow-ups
Stacks on PR #23
Depends on the `zeroclawed-mcp` crate from PR #23.
🤖 Generated with Claude Code