Skip to content

feat(install.sh): build + register zeroclawed-mcp with Claude Code (#32)#26

Closed
bglusman wants to merge 4 commits intomainfrom
feat/wire-mcp-into-agent-configs
Closed

feat(install.sh): build + register zeroclawed-mcp with Claude Code (#32)#26
bglusman wants to merge 4 commits intomainfrom
feat/wire-mcp-into-agent-configs

Conversation

@bglusman
Copy link
Copy Markdown
Owner

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

  1. Build + install — release-build step now includes
    `-p zeroclawed-mcp`; install loop copies it into `$BIN_DIR`
    alongside the other binaries.
  2. Register with Claude Code — second python block writes
    `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

  • opencode + openclaw MCP registration when their config schemas are nailed down (same python-mutation pattern extends cleanly)
  • `describe_secret(name)` tool for richer discovery output

Stacks on PR #23

Depends on the `zeroclawed-mcp` crate from PR #23.

🤖 Generated with Claude Code

bglusman and others added 4 commits April 24, 2026 13:40
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>
Copilot AI review requested due to automatic review settings April 25, 2026 03:01
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.sh to build/install zeroclawed-mcp and register it under mcpServers.zeroclawed-secrets in Claude Code settings.
  • Add a new zeroclawed-mcp crate (stdio MCP server) exposing list_secrets, secret_reference, and add_secret_request.
  • Introduce onecli_client::FnoxClient wrapper and add !secure command 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"] }
Comment on lines +167 to +179
/// `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)?;
Comment on lines +190 to +223
/// `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)
}
Comment on lines +859 to +891
/// 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()
),
}
}
Comment on lines +876 to +878
// `!secure ...` — split off the subcommand word.
let mut parts = trimmed.splitn(3, ' ');
let _lead = parts.next(); // `!secure`
Comment on lines +1204 to +1209
(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();
Comment on lines +1217 to +1223
// 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"
Comment on lines +1971 to +1987
// 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"),
}
}
Comment on lines +64 to +69
/// 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 {
@bglusman
Copy link
Copy Markdown
Owner Author

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.

bglusman added a commit that referenced this pull request Apr 25, 2026
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.
bglusman added a commit that referenced this pull request Apr 25, 2026
…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)
@bglusman
Copy link
Copy Markdown
Owner Author

Subsumed by #44 (squashed to 9ed51fbc on main). All commits from this branch are present in the squash. Closing as redundant rather than merging again.

@bglusman bglusman closed this Apr 25, 2026
bglusman added a commit that referenced this pull request Apr 25, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants