Skip to content

Hybrid: drop-in OpenClaw replacement with XMTP, PARA memory, and multi-user ACL#120

Open
ian wants to merge 158 commits intomainfrom
hybrid-73-new-architecture-clawdbot-inspired-xmtp
Open

Hybrid: drop-in OpenClaw replacement with XMTP, PARA memory, and multi-user ACL#120
ian wants to merge 158 commits intomainfrom
hybrid-73-new-architecture-clawdbot-inspired-xmtp

Conversation

@ian
Copy link
Owner

@ian ian commented Feb 23, 2026

What this is

A full rewrite of the agent runtime as a drop-in replacement for OpenClaw — same SOUL.md, AGENTS.md, MEMORY.md, and skills, no migration needed. On top of OpenClaw parity, this adds decentralized messaging via XMTP, a 3-layer PARA memory system, per-user memory isolation, multi-user ACL, and a channel adapter framework.

What's new

XMTP messaging — the agent lives on a wallet address on the XMTP network. Users DM it from any XMTP-compatible client. No account required beyond a wallet.

3-layer memory system

  • PARA knowledge graph — structured entities (projects/areas/resources/archives) with atomic facts, decay tiers (hot/warm/cold), and fact supersession. Facts are never deleted, only superseded.
  • Daily log — append-only chronological log at .hybrid/memory/logs/YYYY-MM-DD.md
  • Auto memory — structured MEMORY.md with fixed sections (Preferences, Learnings, Decisions, Context, Notes)

Per-user memory isolation — each sender's memory is scoped to their wallet address at .hybrid/memory/users/0x.../MEMORY.md. Access control is defined in ACL.md at the project root.

Hybrid search — vector (sqlite-vec) + BM25 (FTS5) merged at 70/30 weighting across all memory layers.

Scheduler — cron, interval, and one-time jobs persisted to SQLite. Uses precise setTimeout (no polling). Exponential backoff on failures. Scheduled tasks run as agent turns and can deliver results via channel adapters.

Channel adapter framework — uniform interface for message delivery. XMTP today; Telegram, Slack, or webhooks via ChannelAdapter.

ENS + Basename resolution — full forward/reverse resolution for .eth and .base.eth names via viem.

AGENT_SECRET auto-derived — derived from AGENT_WALLET_KEY via BIP-32 at m/44'/60'/0'/0/41. No need to set separately.

hybrid CLIhybrid dev, hybrid build, hybrid deploy fly, hybrid deploy cf, hybrid register, hybrid skills add/remove/list.

Packages

Package Description
hybrid/agent Agent runtime: HTTP server + XMTP sidecar
hybrid/gateway Cloudflare Workers gateway + container lifecycle
@hybrd/memory 3-layer PARA memory, multi-user ACL, hybrid search
@hybrd/scheduler Agentic cron/interval/one-time scheduler
@hybrd/channels Channel adapter framework
@hybrd/xmtp XMTP client, ENS/Basename resolvers
@hybrd/cli hybrid CLI
create-hybrid Project scaffolding (npm create hybrid)

Open with Devin

ian added 3 commits February 22, 2026 19:01
- Migrate from Vocs to Fumadocs (Next.js-based docs framework)
- Add proper RootProvider for client-side interactivity (theme toggle, sidebar)
- Add DataFast analytics script
- Fix home page vertical centering
- Remove old examples, create-hybrid, and ponder packages
- Gateway Worker with per-team Sandbox isolation
- Container server using Claude Agent SDK with query()
- SSE streaming for real-time responses
- R2 bucket for XMTP database storage
- SOUL.md and INSTRUCTIONS.md for agent configuration
@linear
Copy link

linear bot commented Feb 23, 2026

@ian ian changed the title hybrid 73 new architecture clawdbot inspired xmtp Containerized Agents (clawdbot inspiration) Feb 23, 2026
@socket-security
Copy link

socket-security bot commented Feb 23, 2026

@socket-security
Copy link

socket-security bot commented Feb 23, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm @cloudflare/workers-types is 98.0% likely obfuscated

Confidence: 0.98

Location: Package overview

From: packages/gateway/package.jsonnpm/@cloudflare/workers-types@4.20260228.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@cloudflare/workers-types@4.20260228.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm ai is 98.0% likely obfuscated

Confidence: 0.98

Location: Package overview

From: agents/hybrid-agent/package.jsonnpm/ai@6.0.119

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/ai@6.0.119. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm entities is 91.0% likely obfuscated

Confidence: 0.91

Location: Package overview

From: pnpm-lock.yamlnpm/entities@6.0.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/entities@6.0.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@ian ian changed the title Containerized Agents (clawdbot inspiration) Containerized Agents + XMTP Feb 23, 2026
ian added 21 commits February 22, 2026 22:32
- Interactive prompts for project name, XMTP env, agent name
- CLI arguments for non-interactive use: create-hybrid my-agent --env production
- Generates complete Cloudflare Workers + Containers project
- Includes gateway, server, SOUL.md, INSTRUCTIONS.md templates
@ian ian changed the title Containerized Agents + XMTP Hybrid: drop-in OpenClaw replacement with XMTP, PARA memory, and multi-user ACL Mar 3, 2026
ian and others added 17 commits March 2, 2026 21:29
…ing flow

Replace ACL.md with JSON-based access control stored in ./credentials/.
This enables programmatic read/write for mini apps and web interfaces.

Changes:
- Replace ACL.md parsing with JSON files (xmtp-allowFrom.json, xmtp-pairing.json)
- Add pairing flow: request → approve/reject with 8-char codes (1hr expiry)
- 100% compatible with OpenClaw schema (version, allowFrom, requests)
- Add 4 new agent tools: ACLRequestPairing, ACLListPending, ACLApprovePairing, ACLRejectPairing
- Add comprehensive tests (17 tests passing)

File locations:
- ./credentials/xmtp-allowFrom.json — approved owners
- ./credentials/xmtp-pairing.json — pending requests
The XMTP sidecar now filters incoming messages based on the ACL
allowlist stored in ./credentials/xmtp-allowFrom.json.

Changes:
- Add workspaceDir to XMTPAdapterConfig
- Resolve sender address from inboxId using conversation members
- Check sender against allowlist before processing messages
- Display ACL status in adapter banner (e.g., "3 allowed" or "open")
- Block messages from non-allowed senders with warning log

Behavior:
- If allowlist is empty or doesn't exist, all messages are allowed
- If allowlist has entries, only those addresses can trigger the agent
- Address resolution uses conversation members + inboxState fallback
Add vitest test suites for ACL functionality:

Memory package (26 tests):
- parseACL, getRole, listOwners
- addACLAllowFromEntry, removeACLAllowFromEntry
- Pairing flow: request, approve, reject
- Edge cases: multiple owners, concurrent requests, max pending
- Invalid JSON, missing version field, case-insensitive codes

Channels package (8 tests):
- Allowlist reading and checking
- Integration with XMTP adapter filtering logic
- Blocked/allowed sender scenarios
- Empty allowlist behavior
Add test-acl.yml workflow that runs on changes to:
- packages/memory/**
- packages/channels/**

Runs tests for both packages in parallel on pull requests and pushes to main.
- Remove test-core.yml (packages/core doesn't exist)
- Remove plugin.filters.test.ts (XMTP SDK mocking issues)
- AGENT_SECRET is now automatically derived from AGENT_WALLET_KEY using BIP-32
- Removed AGENT_SECRET requirement from all packages
- Fixed hybrid dev to load .env from project directory
- Changed default port from 4100 to 8454
- Fixed import.meta.url issues in CJS bundles
- Fixed fly.io secrets detection (lowercase 'name' field)
- Made hybrid deploy idempotent - checks existing secrets on Fly.io
- Add ensureSkills() helper function
- Copy core skills from packages/agent/skills to .hybrid/skills/core
- Copy user skills from ./skills to .hybrid/skills/ext
- Only copy if skills don't already exist
- Remove build step from hybrid dev (tsx runs TypeScript directly)
- Use concurrently to run server and xmtp sidecar with tsx watch
- Ensure skills are copied before starting
- deploy no longer reads from local .env
- only manages AGENT_WALLET_KEY on Fly.io
- wallet is generated/prompted only if not on Fly.io
- local .env is only for hybrid dev
- added reminder to set other secrets manually
* feat: implement OpenCode template parity

- Add 8 core OpenCode-compatible templates (IDENTITY.md, SOUL.md, AGENTS.md, USER.md, TOOLS.md, BOOT.md, BOOTSTRAP.md, HEARTBEAT.md)
- Update agent server to load all templates with multi-tenant USER.md support
- Add per-user USER.md resolution (users/{userId}/USER.md with fallback to root)
- Update system prompt construction to include all templates in correct order
- Update create-hybrid CLI to scaffold all templates
- Add comprehensive tests for template scaffolding (9 tests)
- Update documentation with template system details

This provides 100% OpenCode compatibility for agent templates.

* feat: implement OpenClaw template parity

- Add 8 core OpenClaw-compatible templates (IDENTITY.md, SOUL.md, AGENTS.md, USER.md, TOOLS.md, BOOT.md, BOOTSTRAP.md, HEARTBEAT.md)
- Update agent server to load all templates with multi-tenant USER.md support
- Add per-user USER.md resolution (users/{userId}/USER.md with fallback to root)
- Update system prompt construction to include all templates in correct order
- Update create-hybrid CLI to scaffold all templates
- Add comprehensive tests for template scaffolding (9 tests)
- Update documentation with template system details

This provides 100% OpenClaw compatibility for agent templates.

Source: https://github.com/openclaw/openclaw/tree/main/docs/reference/templates

* feat: add agents/hybrid-agent template for 'hybrid init'

The 'hybrid init' command copies from agents/hybrid-agent/ to create new agents. This template includes all 8 OpenClaw-compatible template files.

Also includes:
- src/server/index.ts - Agent server with multi-tenant USER.md loading
- src/gateway/index.ts - Cloudflare Workers gateway
- src/dev-gateway.ts - Development gateway
- Dockerfile, wrangler.jsonc, build.mjs - Deployment configs

* fix: exclude agents/hybrid-agent from workspace

The hybrid-agent directory is a template for 'hybrid init', not a real package.
Excluding it from pnpm workspace prevents turbo from running typecheck on it.
* feat: implement two-user process isolation for secret protection

- Add memory-only secret store to protect wallet key from Claude CLI
- Implement per-user workspace isolation with namespaced directories
- Restructure to separate build artifacts (/app) from runtime data (/app/data)
- Add two-user Docker setup (app/claude) with privilege dropping
- Add entrypoint script for secure secret injection from env vars
- Add claude-wrapper for privilege drop with workspace isolation
- Add security test workflow with 9 security validation tests

* Apply 10 edits across 8 files

* Apply 1 edit across 1 file

* Apply 7 edits across 5 files

* Apply 11 edits across 4 files

* Apply 4 edits across 3 files

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 9 additional findings in Devin Review.

Open in Devin Review

Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Reaction handler ignores behavior filter result — calls agent.generate() even when filtered

In the reaction handler at packages/xmtp/src/plugin.ts:130-140, executeBefore writes to a behaviorContext variable that is scoped inside the if (context.behaviors) block (line 131). The filtered check at line 165 uses a different behaviorContext variable declared at line 143 — this second variable is created after agent.generate() runs and never reflects the filter result from executeBefore. Additionally, unlike the text and reply handlers which check behaviorContext.stopped, the reaction handler never checks stopped after executeBefore, so even if a behavior chain stops early, the reaction handler still calls agent.generate() and sends a response. This wastes an LLM API call for every filtered reaction message.

(Refers to lines 130-140)

Prompt for agents
In packages/xmtp/src/plugin.ts, the reaction handler (starting at line 109) needs to be restructured to match the pattern used in the text and reply handlers. Specifically:

1. At line 130-138, declare `behaviorContext` with `let` before the `if` block so it persists after the block.
2. After `executeBefore()` completes (after line 137), add a check for `behaviorContext.stopped` and `behaviorContext.sendOptions?.filtered` — return early if either is true, before calling `agent.generate()`.
3. Reuse the same `behaviorContext` variable for `executeAfter()` instead of creating a new one at line 143.

The text and reply handlers already follow this pattern (see lines 325-341 for the text handler pattern).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +388 to +391
})(
// Store xmtpClient in context for scheduler and other components
context as any
).xmtpClient = xmtpClient
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 XMTP plugin text handler is immediately invoked instead of registered as event listener

The xmtp.on("text", ...) event handler's closing syntax at packages/xmtp/src/plugin.ts:388-391 is })(context as any).xmtpClient = xmtpClient instead of }). This turns the callback into an IIFE (Immediately Invoked Function Expression) — the callback function is called immediately with context as any as its argument, and the return value (a Promise) has .xmtpClient assigned on it. The intent was clearly to register the callback with xmtp.on() and then separately assign context.xmtpClient = xmtpClient, but the parenthesization is wrong. As a result: (1) the text handler fires immediately once with context as its argument (which doesn't have conversation or message properties, causing a crash), and (2) no text handler is actually registered on the XMTP agent, so incoming text messages are never processed.

Suggested change
})(
// Store xmtpClient in context for scheduler and other components
context as any
).xmtpClient = xmtpClient
})
// Store xmtpClient in context for scheduler and other components
;(context as any).xmtpClient = xmtpClient
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

ian added 2 commits March 5, 2026 11:49
- Rename config output from agent.config.js → hybrid.config.js
- Auto-migrate openclaw.json to hybrid.config.ts (one-way migration)
- Preserve original openclaw.json as backup
- Fallback to agent.ts (legacy)
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 15 additional findings in Devin Review.

Open in Devin Review

Comment on lines +140 to +161
export function isPathInUserWorkspace(
workspaceDir: string,
userId: string,
path: string
): boolean {
const sanitizedUserId = sanitizeUserId(userId)
const dataRoot = getDataRoot()
const expectedWorkspace = join(dataRoot, "workspaces", sanitizedUserId)
const expectedMemory = join(dataRoot, "memory", "users", sanitizedUserId)

// Normalize paths to prevent traversal
const normalizedPath = join(workspaceDir, path)

// Check if path is within user's workspace or memory
// Use trailing separator to prevent prefix collisions (e.g. alice vs alicebob)
return (
normalizedPath === expectedWorkspace ||
normalizedPath.startsWith(expectedWorkspace + "/") ||
normalizedPath === expectedMemory ||
normalizedPath.startsWith(expectedMemory + "/")
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Path traversal in isPathInUserWorkspace: join(workspaceDir, path) does not prevent .. traversal

In packages/agent/src/lib/workspace.ts:151, the path validation uses join(workspaceDir, path) but path.join resolves .. segments. For example, join("/app/data/workspaces/alice", "../../secrets/wallet.key") resolves to /app/data/secrets/wallet.key, which passes the startsWith(expectedWorkspace) check if workspaceDir contains a .. prefix that resolves to a parent. However, the actual vulnerability is that join normalizes .. and the resulting path won't start with the expected workspace prefix — so the validation would correctly reject it. The real issue is that this function takes workspaceDir as a parameter but then re-computes the expected path from dataRoot independently — so if workspaceDir is ever not the canonical path, the check becomes unreliable.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +603 to +618
const SENSITIVE_ENV_KEYS = [
"AGENT_WALLET_KEY",
"AGENT_SECRET",
"WALLET_KEY",
"PRIVATE_KEY",
"SECRET",
"SECRETS_PATH",
"DATA_ROOT"
]

// Build filtered environment for Claude processes
const safeEnv = Object.fromEntries(
Object.entries(process.env).filter(
([key]) => !SENSITIVE_ENV_KEYS.includes(key)
)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Security: OPENROUTER_API_KEY leaks to Claude child processes via env filtering allowlist approach

In packages/agent/src/server/index.ts:603-618, the SENSITIVE_ENV_KEYS list filters specific keys, but uses an allowlist-exclusion approach: any key NOT in the list gets passed through. OPENROUTER_API_KEY is not in the SENSITIVE_ENV_KEYS list, so it is passed to Claude child processes via safeEnv. While ANTHROPIC_AUTH_TOKEN (which is set to the same value) is intentionally passed for API access, the original OPENROUTER_API_KEY env var also leaks. This could allow a compromised Claude process to exfiltrate the API key. The security model described in the DEPLOYMENT.md and architecture docs claims secrets are filtered from Claude processes.

Suggested change
const SENSITIVE_ENV_KEYS = [
"AGENT_WALLET_KEY",
"AGENT_SECRET",
"WALLET_KEY",
"PRIVATE_KEY",
"SECRET",
"SECRETS_PATH",
"DATA_ROOT"
]
// Build filtered environment for Claude processes
const safeEnv = Object.fromEntries(
Object.entries(process.env).filter(
([key]) => !SENSITIVE_ENV_KEYS.includes(key)
)
)
// Sensitive keys that should NEVER be passed to Claude child processes
const SENSITIVE_ENV_KEYS = [
"AGENT_WALLET_KEY",
"AGENT_SECRET",
"WALLET_KEY",
"PRIVATE_KEY",
"SECRET",
"SECRETS_PATH",
"DATA_ROOT",
"OPENROUTER_API_KEY"
]
// Build filtered environment for Claude processes
const safeEnv = Object.fromEntries(
Object.entries(process.env).filter(
([key]) => !SENSITIVE_ENV_KEYS.includes(key)
)
)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

ian added 5 commits March 5, 2026 16:11
…ism for user skills

- Remove .claude/skills from findSkillDir search paths (wrong tool)
- User skills in ./skills/ now override core skills with same name
- Build shows '(overrides core)' when user skill shadows core skill
- Add comprehensive test suite for CLI (21 tests)
The database encryption key is now derived automatically from AGENT_WALLET_KEY via BIP-32, eliminating the need for a separate AGENT_SECRET environment variable. This simplifies configuration and reduces the attack surface.
- Add workspace state tracking for onboarding progress
- Track bootstrapSeededAt and onboardingCompletedAt in .hybrid/workspace-state.json
- Reject non-owners during onboarding mode
- Load BOOTSTRAP.md as context file (not system prompt)
- Auto-detect onboarding completion when BOOTSTRAP.md deleted
- Scaffold ACL.md with wallet placeholder for owner access
- Add comprehensive tests for state management
- Update README with onboarding documentation
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 new potential issues.

View 40 additional findings in Devin Review.

Open in Devin Review

Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Reaction handler runs agent.generate() before checking if message was filtered

In the reaction event handler (packages/xmtp/src/plugin.ts:109-181), executeBefore() is called on line 137, but the filter check (behaviorContext?.sendOptions?.filtered) only happens on line 165 — after agent.generate() has already been called on line 140. This means the agent wastes an expensive LLM call generating a response for messages that should have been filtered out by the behavior chain. The text and reply handlers correctly check behaviorContext.stopped before calling agent.generate(), but the reaction handler does not.

(Refers to lines 138-170)

Prompt for agents
In packages/xmtp/src/plugin.ts, the reaction handler (lines 109-181) calls agent.generate() on line 140 BEFORE checking if the message was filtered on line 165. Move the filter/stopped check to immediately after the executeBefore() call on line 137, before the agent.generate() call on line 140. Follow the same pattern used in the text handler (lines 334-341) and reply handler (lines 216-221) which check behaviorContext.stopped right after executeBefore() and return early.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +388 to +391
})(
// Store xmtpClient in context for scheduler and other components
context as any
).xmtpClient = xmtpClient
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 XMTP plugin text handler is immediately invoked instead of registered as callback

The xmtp.on("text", ...) handler at packages/xmtp/src/plugin.ts:270-391 is immediately invoked due to a malformed expression. The closing }) of the arrow function on line 387 is followed by (context as any).xmtpClient = xmtpClient on lines 388-391, turning the event handler into an IIFE (Immediately Invoked Function Expression). This means:

  1. The async ({ conversation, message }) handler is called immediately with context as any as its argument (not a valid {conversation, message} object), which will crash at runtime.
  2. The .on("text", ...) call receives the return value of the IIFE (which is undefined from the catch block), instead of receiving the handler function.
  3. The text event handler is effectively never registered — the agent will not respond to any text messages.
Code structure causing the bug
// Line 270: handler starts
xmtp.on("text", async ({ conversation, message }: any) => {
  // ... handler body ...
})(                            // Line 388: IIFE - immediately calls the handler!
  context as any               // Line 390: passes context as the argument
).xmtpClient = xmtpClient      // Line 391: assigns to return value

The intent was clearly to write (context as any).xmtpClient = xmtpClient as a separate statement, but the closing parenthesis of xmtp.on(...) was broken up, turning the callback into an IIFE.

Suggested change
})(
// Store xmtpClient in context for scheduler and other components
context as any
).xmtpClient = xmtpClient
})
// Store xmtpClient in context for scheduler and other components
;(context as any).xmtpClient = xmtpClient
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +648 to +655
const SENSITIVE_ENV_KEYS = [
"AGENT_WALLET_KEY",
"WALLET_KEY",
"PRIVATE_KEY",
"SECRET",
"SECRETS_PATH",
"DATA_ROOT"
]
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Sensitive env var filtering does not block OPENROUTER_API_KEY from leaking to Claude child processes

In packages/agent/src/server/index.ts:648-655, the SENSITIVE_ENV_KEYS list filters env vars passed to Claude child processes. However, OPENROUTER_API_KEY is not in this list. Since the auto-configure block at line 43-47 copies OPENROUTER_API_KEY into process.env.ANTHROPIC_AUTH_TOKEN and the latter is intentionally re-added to envVars at line 669, the OPENROUTER_API_KEY env var itself (which is still in process.env) will leak through safeEnv to Claude child processes. This means Claude CLI can access the API key from its environment, undermining the security goal of the sensitive env filtering.

Suggested change
const SENSITIVE_ENV_KEYS = [
"AGENT_WALLET_KEY",
"WALLET_KEY",
"PRIVATE_KEY",
"SECRET",
"SECRETS_PATH",
"DATA_ROOT"
]
// Sensitive keys that should NEVER be passed to Claude child processes
const SENSITIVE_ENV_KEYS = [
"AGENT_WALLET_KEY",
"WALLET_KEY",
"PRIVATE_KEY",
"SECRET",
"SECRETS_PATH",
"DATA_ROOT",
"OPENROUTER_API_KEY"
]
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

1 participant