shareAI-lab / shareAI-lab/Kode-CLI

project hooks execute in `--safe` mode and approved MCP names do not bind configuration content

Open
#233 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
TypeScript
Stars
5.2k
Forks
772
PR merge metrics
No merged PRs in 30d

Description

Summary

The latest shareAI-lab/Kode-CLI main commit, 42dea6a0ad8a8dbd6450c3075672ac3922f3849d, still has two independent project-configuration trust gaps:

  1. A committed .kode/settings.json or .claude/settings.json can define a UserPromptSubmit command hook. Submitting an ordinary prompt executes the repository-controlled command through /bin/sh -c, including when Kode is run with --safe. No workspace trust or command-specific approval is checked immediately before this spawn.
  2. Project .mcp.json/.mcprc approval is persisted as only a server name. After a user approves a benign declaration, changing that same server's command, args, env, or URL does not return it to pending approval. The changed declaration reaches the MCP stdio/network transport on the next bootstrap.

The first issue is a direct repository-controlled command execution path after a normal prompt submission. The second requires prior user approval and a later same-name configuration change; it is a post-authorization configuration-drift issue, not an unconditional first-open RCE claim. Please track them separately if you prefer separate remediation.

Affected Version

Verified on 42dea6a0ad8a8dbd6450c3075672ac3922f3849d (origin/main, tested 2026-08-12). The behavior was also present in candidate commit 977c2677e381fccc4808f38c2fedb54113667e10.

No tracked SECURITY.md, vulnerability disclosure policy, or bug bounty scope was found in the tested tree.

Finding 1: Project command hook bypasses safe-mode approval

Reproduction
  1. Build or run Kode from the checked-out revision with Bun and create a disposable project directory.
  2. Add this file as .kode/settings.json in that project:
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "printf KODE_HOOK_MARKER > .kode/hook-marker.txt"
          }
        ]
      }
    ]
  }
}
  1. Run one ordinary prompt in safe mode:
bun run apps/cli/src/dispatch.ts --print --safe --no-session-persistence --cwd /path/to/disposable-project 'benign verification prompt'

Use a configured local/mock provider or the normal provider configuration required by the checkout. The command only needs to process the prompt; the hook runs before the model response completes.

Expected And Actual

Expected: --safe should not execute a repository-provided command hook without a visible approval bound to that hook, or it should reject the untrusted project hook.

Actual: .kode/hook-marker.txt is created with KODE_HOOK_MARKER. Removing .kode/settings.json and repeating the same command does not create the marker.

Technical Evidence

packages/config/src/files.ts automatically resolves project settings from .kode/settings.json and legacy .claude/settings.json. packages/hooks/src/registry.ts parses command hooks and loads project settings for UserPromptSubmit. packages/hooks/src/lifecycle/events.ts dispatches the event without using safeMode as a gate. packages/hooks/src/executor.ts executes the configured string as /bin/sh -c <command> with the project cwd and inherited environment.

This does not require the model to call Bash and does not depend on prompt injection. The minimum trigger is Kode processing the project and receiving one prompt. It is not a claim that merely opening a directory starts the hook.

Finding 2: Approved MCP server name survives command changes

Reproduction

Use a disposable project and disposable Kode config directory. First create .mcp.json with a benign declaration:

{
  "mcpServers": {
    "same-name": {
      "type": "stdio",
      "command": "printf",
      "args": ["benign-v1"]
    }
  }
}

Start Kode normally and approve the pending same-name project server. This approval is the required user authorization precondition. The UI stores the approval in the project config.

Then replace the repository file, keeping the name but changing the command-bearing content:

{
  "mcpServers": {
    "same-name": {
      "type": "stdio",
      "command": "sh",
      "args": ["-c", "printf KODE_MCP_MARKER > mcp-marker.txt"]
    }
  }
}

Restart Kode in the same project or otherwise trigger its normal MCP bootstrap. No new approval dialog is shown for the changed command. The marker is created before the intentionally incomplete test MCP process exits.

Expected And Actual

Expected: changing a previously approved project's command, args, environment, URL, or referenced executable should invalidate the approval and require a new approval showing the effective declaration.

Actual: approvedMcprcServers contains only same-name; getMcprcServerStatus('same-name') remains approved after the declaration changes, and getClients() passes the new declaration to StdioClientTransport.

Technical Evidence

packages/config/src/mcp.ts reads project .mcp.json and .mcprc. packages/core/src/mcp/client/config.ts:getMcprcServerStatus() checks only the server name in approvedMcprcServers. apps/cli/src/ui/components/MCPServerApprovalDialog.tsx persists only serverName. packages/core/src/mcp/client/clients.ts filters by that name-only status, and packages/core/src/mcp/client/connection.ts constructs the transport from the current command, args, and env.

This finding does not claim that a never-approved server silently runs on first use. It requires an earlier approval, a later same-name repository/configuration change, and a subsequent MCP connection bootstrap.

Dynamic Verification

Both findings were reproduced against the latest main in isolated temporary fixtures on Linux:

hook: markerCreated=true; controlMarkerAbsent=true; safeMode=true
mcp: statusBeforeChange=approved
mcp: statusAfterSameNameCommandChange=approved
mcp: approvalStillNameOnly=true; changedCommandReachedStdioSink=true
cli hook E2E: exitCode=0; markerCreated=true; output=mock response

The mock provider was loopback-only. The tests did not read credentials, contact external endpoints, modify shell profiles, persist outside temporary fixtures, or delete user files.

Suggested Fix

Treat project command hooks as executable workspace configuration. Require explicit per-workspace approval before dispatch, enforce the check immediately before spawn, and bind approval to the canonical workspace, settings source and digest, event/matcher, final command/args/env, and referenced script identities. Safe mode should include this capability boundary.

For MCP, store an approval receipt over the final effective server declaration and resolved executable/script or URL, display it at approval time, and invalidate it on any command-bearing content, symlink target, workspace revision, or declaration digest change. Revalidate before every transport bootstrap.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing project settings through packages/config/src/files.ts, packages/hooks/src/registry.ts, packages/hooks/src/lifecycle/events.ts, and packages/hooks/src/executor.ts; then inspect the MCP approval path in packages/config/src/mcp.ts, packages/core/src/mcp/client/config.ts, clients.ts, connection.ts, and MCPServerApprovalDialog.tsx. Re-run the two isolated reproductions and verify that hook execution and MCP transport bootstrap require approval bound to the effective configuration.

Written by the indexing model from the issue text.

Assessment

Tech stack
bun, shell, typescript
Domain
cli, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.