# Defense-in-Depth Score: CowAgent

**Repo:** https://github.com/zhayujie/CowAgent · **Commit:** `2b2d11270ae6472358242e4926fbf1d8f0d21b11` (2.2.0) · **Reviewed:** 2026-10-03
**What it is:** Personal AI assistant & agent harness that plans tasks and runs tools/skills across chat apps
**Category:** AI Assistants
**Scored configuration:** One-line installer / source run with the shipped config-template.json: web console channel bound to 127.0.0.1 with no password, agent mode on, agent_permission_mode full-access, self-evolution on.
**Agent surface (default):** code execution yes · filesystem write yes · network egress yes · external credentials yes · persistent memory yes · untrusted input yes · third party extensions yes · sub agents yes · external communication yes

## Score: 1.7 / 10.0 (Minimal)

| # | Criterion | S | C | D | B | Raw | Cap | Score | Confidence |
|---|---|---|---|---|---|---|---|---|---|
| C1 | Identity & least privilege | L0 | L0 | L0 | L0 | 0.00 | — | **0.00** | High |
| C2 | Approval gates | L0 | L0 | L0 | L1 | 0.05 | — | **0.05** | High |
| C3 | Tool & action scoping | L2 | L2 | L1 | L0 | 0.35 | G2 | **0.25** (alt) | High |
| C4 | Code-execution isolation | L2 | L3 | L2 | L0 | 0.47 | G1 | **0.47** (alt) | High |
| C5 | Untrusted input blast radius | L0 | L0 | L0 | L0 | 0.00 | C5-WORSTCASE | **0.00** | High |
| C6 | Memory, context & configuration integrity | L0 | L0 | L0 | L0 | 0.00 | C6-REPOCONFIG | **0.00** | High |
| C7 | Third-party extensions | L0 | L0 | L0 | L0 | 0.00 | C7-RCELOAD | **0.00** | High |
| C8 | Secrets & sensitive-data protection | L0 | L1 | L1 | L0 | 0.12 | — | **0.12** | High |
| C9 | Audit & traceability | L2 | L2 | L1 | L1 | 0.40 | — | **0.40** | High |
| C10 | Limits & kill switch | L2 | L2 | L1 | L1 | 0.40 | — | **0.40** | High |


As shipped, CowAgent is an unsandboxed shell-and-browser agent running as your user, in full-access mode, with no human approval for any action and every API key and chat-app secret in the environment of the commands it runs. Anything it reads, whether a web page, a file or a message from someone in a connected chat, can make it leak those keys and act irreversibly. The agent can also give itself new MCP servers and persistent instructions through files in its own workspace. The optional workspace-write and read-only modes and the Docker deployment help, but the modes are not a complete boundary.

## Critical gaps
- A hijacked agent holds the user's whole local account plus every model and chat-platform credential, which config.py exports into the environment of every shell command. (ASI03, T3; C1) — [agent/tools/bash/bash.py:167](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L167); [config.py:698-702](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L698-L702)
- Model-written shell commands run on the host as the user with shell=True and the full credential-bearing environment. (ASI05, T11; C4) — [agent/tools/bash/bash.py:456-465](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L456-L465); [agent/tools/bash/bash.py:167](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L167)
- Untrusted content can drive both secret exfiltration and irreversible actions with no human in the loop (C5-WORSTCASE). (ASI01, T6, LLM01; C5) — [agent/protocol/agent_stream.py:446-447](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/protocol/agent_stream.py#L446-L447); [agent/tools/utils/url_safety.py:4-7](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/utils/url_safety.py#L4-L7); [agent/tools/bash/bash.py:47](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L47)
- mcp.json in the agent's own working directory is hot-reloaded, so the agent or poisoned content can add and launch MCP servers without a trust decision (C6-REPOCONFIG). (ASI06, T1; C6) — [common/state_dir.py:161-162](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/common/state_dir.py#L161-L162); [bridge/agent_bridge.py:1888-1890](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/bridge/agent_bridge.py#L1888-L1890); [bridge/agent_initializer.py:947](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/bridge/agent_initializer.py#L947)
- An agent-written mcp.json entry launches arbitrary commands with no consent, and the MCP command allowlist is empty by default (C7-RCELOAD). (ASI04, T17; C7) — [bridge/agent_bridge.py:1888-1890](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/bridge/agent_bridge.py#L1888-L1890); [agent/tools/mcp/mcp_client.py:335-336](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/mcp/mcp_client.py#L335-L336); [agent/tools/mcp/mcp_client.py:275-276](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/mcp/mcp_client.py#L275-L276)
- Skill code runs through bash with the full environment, including all API keys. (ASI04, T17; C7) — [agent/tools/bash/bash.py:167](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L167); [agent/skills/manager.py:174](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/skills/manager.py#L174)

## Criterion details

### C1 Identity & least privilege — 0.00 (high)

CowAgent runs every tool as the operating-system user that launched it, with no identity of its own. At startup it copies model API keys and chat-platform app secrets (Feishu, DingTalk, WeChat, QQ) from config into the process environment, and the shell tool hands that whole environment, plus ~/.cow/.env, to every command; the tool description even tells the model the keys are available as $VARS. The optional permission modes are off in the shipped config (full-access). A hijacked agent therefore holds the user's whole local account plus every connected service credential.

- **S L0:** Tools run with the OS user's ambient authority and the bash subprocess receives a full copy of the process environment, including API keys and channel app secrets synced from config. — [agent/tools/bash/bash.py:167](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L167); [agent/tools/bash/bash.py:199-206](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L199-L206); [config.py:698-702](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L698-L702); [config.py:681](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L681); [agent/tools/bash/bash.py:47](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L47) (verified)
  - *To reach the next level:* A dedicated or role-scoped identity for the agent's tools rather than the launching user's full environment and credentials.
- **C L0:** No authorization layer is applied in the default full-access mode; the policy check returns ALLOW for every tool, and only the prompt asks the model to confirm destructive commands. — [agent/permission/policy.py:454-455](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/permission/policy.py#L454-L455); [agent/tools/bash/bash.py:49-51](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L49-L51) (verified)
  - *To reach the next level:* A code-level check on the main tool path; currently bash, write, browser and MCP tools all run unchecked.
- **D L0:** Default install (source and Docker) ships agent_permission_mode full-access; only a first-launch desktop client is tightened to workspace-write. — [config.py:315](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L315); [config-template.json:42](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config-template.json#L42); [config.py:567-568](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L567-L568); [docker/docker-compose.yml:45-46](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/docker/docker-compose.yml#L45-L46) (verified)
  - *To reach the next level:* A narrower default role (e.g. workspace-write or read-only) for all install types.
- **B L0:** A hijacked agent can use the user's entire local account (home directory, SSH keys, cloud CLIs) and every configured model and chat-platform credential. — [agent/tools/bash/bash.py:167](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L167); [config.py:698-702](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L698-L702); [config.py:681](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L681) (verified)
  - *To reach the next level:* Credentials limited to one system or read-mostly scope.
- **Cap:** none

### C2 Approval gates — 0.05 (high)

There is no human approval step anywhere in the tool path: shell commands, file writes, browser clicks, scheduled tasks and outbound sends execute as soon as the model asks. The only safeguards are prompt text telling the model to confirm destructive operations and a tiny shell denylist that returns an error. Most actions are irreversible; only self-evolution edits to memory and skills are snapshotted for undo.

- **S L0:** No approval mechanism exists; the bash tool only asks the model, via prompt text, to confirm destructive commands. — searched `rg -n -i "approv"` in `agent bridge` → 2 hits (Hits are the MCP verb regex in policy.py and a comment in evolution/executor.py; there is no human approval gate on any tool.); [agent/tools/bash/bash.py:49-51](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L49-L51) (verified)
  - *To reach the next level:* Per-call human approval of consequential actions.
- **C L0:** The most powerful tool, bash (shell=True on the host), runs without any gate. — [agent/tools/bash/bash.py:456-465](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L456-L465); [agent/protocol/agent_stream.py:2086](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/protocol/agent_stream.py#L2086); [agent/permission/policy.py:454-455](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/permission/policy.py#L454-L455) (verified)
  - *To reach the next level:* At least the shell, write and outbound-communication tools passing through a human gate.
- **D L0:** Since no approval mechanism exists, nothing is on by default. — searched `rg -n -i "approv"` in `agent bridge` → 2 hits (Hits are the MCP verb regex in policy.py and a comment in evolution/executor.py; there is no human approval gate on any tool.) (verified)
  - *To reach the next level:* Approval on by default for consequential tools.
- **B L1:** A wrongly executed action is mostly irreversible (rm, curl, browser form submission, channel sends); only self-evolution edits are backed up and undoable. — [agent/evolution/executor.py:9](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/evolution/executor.py#L9); [agent/tools/evolution_undo/evolution_undo.py:1-5](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/evolution_undo/evolution_undo.py#L1-L5) (verified)
  - *To reach the next level:* Reversibility for the common case, e.g. checkpoints of workspace files before write/edit/bash.
- **Cap:** none

### C3 Tool & action scoping — 0.25 (high)

In the shipped full-access mode every tool is enabled and the shell, web fetch and browser tools accept arbitrary commands and URLs; the only argument checks are a denylist that blocks the ~/.cow/.env credential file and /proc environ paths, and SSRF protection that is off by default. The optional workspace-write and read-only modes add real path containment for write/edit, but they do not cover every path. Those modes are off by default.

- **default configuration** (default; raw 0.15 → 0.15)
  - **S L1:** Argument validation in the default mode is a denylist: file tools refuse the credential file and /proc/*/environ, bash refuses a regex match on .cow/.env; URLs are unchecked because the SSRF guard is opt-in. — [agent/tools/utils/credentials.py:31-57](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/utils/credentials.py#L31-L57); [agent/tools/bash/bash.py:153](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L153); [agent/tools/utils/url_safety.py:4-7](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/utils/url_safety.py#L4-L7); [agent/tools/web_fetch/web_fetch.py:124-125](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/web_fetch/web_fetch.py#L124-L125) (verified)
    - *To reach the next level:* Allowlist validation (resolved-path containment, host allowlists) applied by default.
  - **C L1:** Only the file tools apply the credential-path denylist; bash, web_fetch, browser and MCP tools pass arguments through. — [agent/tools/utils/credentials.py:31-57](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/utils/credentials.py#L31-L57); [agent/tools/web_fetch/web_fetch.py:124-125](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/web_fetch/web_fetch.py#L124-L125); [agent/tools/utils/url_safety.py:4-7](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/utils/url_safety.py#L4-L7) (verified)
    - *To reach the next level:* Validation on most built-in tools, including bash and web_fetch.
  - **D L0:** Every tool, including bash, write, browser, scheduler and web_fetch, is enabled by default with full-access permissions. — [config.py:315](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L315); [config-template.json:42](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config-template.json#L42) (verified)
    - *To reach the next level:* Dangerous tools individually disableable or grouped, with a narrower default set.
  - **B L0:** A misused bash call can run any command against the whole machine as the user. — [agent/tools/bash/bash.py:456-465](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L456-L465); [agent/permission/policy.py:454-455](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/permission/policy.py#L454-L455) (verified)
    - *To reach the next level:* Tool reach scoped to a workspace.
- **opt-in permission modes (agent_permission_mode workspace-write / read-only)** (alt; raw 0.35, cap G2 → 0.25) ← counted
  - **S L2:** write/edit targets are realpath-resolved and checked with commonpath against write roots; the shell is not confined to the same roots. — [agent/permission/policy.py:365-396](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/permission/policy.py#L365-L396) (verified)
    - *To reach the next level:* Containment that holds on every tool path.
  - **C L2:** The check runs in the executor for every tool call, but in workspace-write only write/edit/bash are examined; MCP tools are judged by a name regex only in read-only mode. — [agent/protocol/agent_stream.py:2086](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/protocol/agent_stream.py#L2086); [agent/permission/policy.py:521-524](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/permission/policy.py#L521-L524); [agent/permission/policy.py:492-497](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/permission/policy.py#L492-L497) (verified)
    - *To reach the next level:* Uniform validation for all tools, including MCP and browser actions, through the same policy layer.
  - **D L1:** The modes are opt-in and are not a complete boundary. — [config.py:315](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L315) (verified)
    - *To reach the next level:* Mode on by default, with hardened enforcement.
  - **B L0:** If the mode is bypassed the agent again has any command on the whole machine. — [agent/tools/bash/bash.py:456-465](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L456-L465) (verified)
    - *To reach the next level:* Workspace-scoped reach that survives a bypass of the argument checks (e.g. via OS isolation).
- **Cap:** G2 — The mode is not a complete boundary.

### C4 Code-execution isolation — 0.47 (high)

Model-written shell commands run directly on the host as the user through subprocess with shell=True, with the full environment and no sandbox. The only filter is a deliberately minimal denylist (rm -rf /, dd from /dev/zero, shutdown) that tells the model to ask the user, which other phrasings evade. The documented Docker deployment runs the whole app in a non-root container, which is a real but basic boundary: the compose file disables seccomp, mounts the data directory holding config and credentials, passes API keys in the container environment, and leaves network egress open.

- **default configuration** (default; raw 0.25 → 0.25)
  - **S L1:** Host execution guarded only by a minimal command denylist. — [agent/tools/bash/bash.py:456-465](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L456-L465); [agent/tools/bash/bash.py:558-604](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L558-L604) (verified)
    - *To reach the next level:* OS-level separation (container, low-privilege user) for model-run commands.
  - **C L1:** The denylist covers foreground and background bash, but browser JavaScript evaluation and MCP stdio servers run on the host unfiltered. — [agent/tools/bash/bash.py:159-163](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L159-L163); [agent/tools/mcp/mcp_client.py:275-276](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/mcp/mcp_client.py#L275-L276); [agent/tools/browser/browser_tool.py:72](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/browser/browser_tool.py#L72) (verified)
    - *To reach the next level:* Every execution path, including MCP stdio launches and browser evaluate, behind the same control.
  - **D L2:** The denylist is on by default but disabled by the tool config key safety_mode without warning. — [agent/tools/bash/bash.py:88](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L88) (verified)
    - *To reach the next level:* Disabling requires an explicit operator flag, with escalation as per-call human approval.
  - **B L0:** Commands run host-equivalent with the user's home directory and every API key in their environment. — [agent/tools/bash/bash.py:167](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L167); [config.py:698-702](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L698-L702) (verified)
    - *To reach the next level:* At most a broad mount without credentials in the environment.
- **Docker Compose deployment (whole app in a container)** (alt; raw 0.47, cap G1 → 0.47) ← counted
  - **S L2:** A stock container running as a dedicated non-root user, with seccomp explicitly disabled. — [docker/Dockerfile.latest:59](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/docker/Dockerfile.latest#L59); [docker/docker-compose.yml:6-7](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/docker/docker-compose.yml#L6-L7) (verified)
    - *To reach the next level:* A hardened profile: seccomp kept, capabilities dropped, no-new-privileges, read-only root.
  - **C L3:** Because the whole application runs in the container, bash, MCP stdio servers and the browser all execute inside it. — [docker/docker-compose.yml:45-46](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/docker/docker-compose.yml#L45-L46); [docker/docker-compose.yml:3-4](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/docker/docker-compose.yml#L3-L4) (verified)
    - *To reach the next level:* Fail-closed coverage with no host fallback documented as such, including spawned extension processes under the same policy.
  - **D L2:** The container boundary is the deployment itself and the model cannot leave it, but it is a separate opt-in deployment and the compose file sets the agent to full-access inside it. — [docker/docker-compose.yml:45-46](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/docker/docker-compose.yml#L45-L46); [docker/docker-compose.yml:6-7](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/docker/docker-compose.yml#L6-L7) (verified)
    - *To reach the next level:* Disabling the boundary needing an explicit flag, with policy defined outside model reach and a hardened default.
  - **B L0:** Inside the container the agent has API keys in its environment, the config and credential directory mounted read-write, and unrestricted network. — [docker/docker-compose.yml:14](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/docker/docker-compose.yml#L14); [docker/docker-compose.yml:55-58](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/docker/docker-compose.yml#L55-L58) (verified)
    - *To reach the next level:* No secrets in the container environment, workspace-only mounts and allowlisted egress.
- **Cap:** G1 — Opt-in mechanism: off in the scored default configuration.

### C5 Untrusted input blast radius — 0.00 (high)

Web pages, search results, files, MCP outputs and (when chat channels are connected) other people's messages enter the model context as ordinary tool results or user turns, with no marking, filtering or change in privileges. Once hijacked, the agent can read the user's secrets and send them anywhere with web_fetch or curl, and can delete files or post through the browser and chat tools, all without a human in the loop. Connected chat channels make this worse: Telegram, Slack, Discord and others whitelist all groups and have no sender allowlist, so anyone who can message the bot can instruct a full-access agent.

- **S L0:** Nothing limits a hijacked agent; untrusted content is not tracked and no capability is removed after it is read. — searched `rg -n -i "untrusted|prompt.injection"` in `agent bridge` → 1 hits (The only hit is a comment in agent/evolution/backup.py about manifest paths; nothing marks or isolates untrusted tool/web content.); [agent/protocol/agent_stream.py:446-447](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/protocol/agent_stream.py#L446-L447) (verified)
  - *To reach the next level:* At least approval for some dangerous capabilities once untrusted content has been read.
- **C L0:** Untrusted sources are not distinguished from the user's instructions; tool results are appended as ordinary user-role tool_result blocks. — [agent/protocol/agent_stream.py:446-447](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/protocol/agent_stream.py#L446-L447); searched `rg -n -i "untrusted|prompt.injection"` in `agent bridge` → 1 hits (The only hit is a comment in agent/evolution/backup.py about manifest paths; nothing marks or isolates untrusted tool/web content.) (verified)
  - *To reach the next level:* Distinguishing at least one untrusted source (e.g. web pages).
- **D L0:** No defence exists, so nothing is on by default. — searched `rg -n -i "untrusted|prompt.injection"` in `agent bridge` → 1 hits (The only hit is a comment in agent/evolution/backup.py about manifest paths; nothing marks or isolates untrusted tool/web content.) (verified)
  - *To reach the next level:* A defence that is on by default.
- **B L0:** A hijacked agent can exfiltrate secrets (arbitrary URL fetch, curl) and take irreversible actions (rm, browser submit, channel send) unattended. — [agent/tools/bash/bash.py:47](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L47); [agent/tools/web_fetch/web_fetch.py:124-125](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/web_fetch/web_fetch.py#L124-L125); [agent/tools/utils/url_safety.py:4-7](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/utils/url_safety.py#L4-L7); [agent/tools/bash/bash.py:456-465](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L456-L465); [channel/telegram/telegram_channel.py:71](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/channel/telegram/telegram_channel.py#L71) (verified)
  - *To reach the next level:* Requiring human approval for at least one of exfiltration or irreversible actions.
- **Cap:** C5-WORSTCASE — In the default configuration a hijacked agent can both leak secrets and take irreversible actions with no human involved.

### C6 Memory, context & configuration integrity — 0.00 (high)

AGENT.md, USER.md, RULE.md and MEMORY.md in the agent's workspace are injected into the system prompt on every turn, and the model writes to them freely, as does an unattended self-evolution pass that is on in the template (it is snapshotted for undo). The same workspace, which is the tools' working directory, holds mcp.json; a change to that file is hot-reloaded and launches new MCP servers with no prompt. Memory has no per-user isolation yet, so in a shared chat one participant's injected memory influences everyone.

- **S L0:** The model can write anything into MEMORY.md/RULE.md, which are re-injected as system-prompt context, and can add MCP servers by editing the workspace mcp.json with no prompt. — [agent/prompt/workspace.py:125-131](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/prompt/workspace.py#L125-L131); [common/state_dir.py:161-162](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/common/state_dir.py#L161-L162); [bridge/agent_bridge.py:1888-1890](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/bridge/agent_bridge.py#L1888-L1890); [bridge/agent_initializer.py:947](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/bridge/agent_initializer.py#L947) (verified)
  - *To reach the next level:* Memory entries presented as data with provenance, and project config unable to add tools or change security settings.
- **C L0:** No memory store, instruction file or config file is write-controlled; self-evolution backups record changes but do not gate them. — [agent/evolution/executor.py:42](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/evolution/executor.py#L42); [agent/evolution/executor.py:9](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/evolution/executor.py#L9); [agent/prompt/workspace.py:125-131](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/prompt/workspace.py#L125-L131) (verified)
  - *To reach the next level:* Control over at least the main memory store.
- **D L0:** There is no per-user namespace: user_id stays None, so memory is shared across everyone who talks to an agent. — [common/runtime_identity.py:31](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/common/runtime_identity.py#L31) (verified)
  - *To reach the next level:* Per-user or per-session namespaces enforced in code by default.
- **B L0:** Poisoned memory or mcp.json persists across sessions and users and can trigger tool use, including launching new processes. — [common/runtime_identity.py:31](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/common/runtime_identity.py#L31); [bridge/agent_bridge.py:1888-1890](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/bridge/agent_bridge.py#L1888-L1890); [config-template.json:50](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config-template.json#L50) (verified)
  - *To reach the next level:* Poisoned context limited to one user's sessions.
- **Cap:** C6-REPOCONFIG — mcp.json in the workspace the agent operates in is hot-reloaded and can add MCP servers (arbitrary commands) without any user trust decision.

### C7 Third-party extensions — 0.00 (high)

Third-party code arrives as skills (from the Skill Hub, any GitHub/GitLab branch or a raw URL) and as MCP servers launched from mcp.json. Nothing is pinned, and the skill checksum is supplied by the same server that serves the download. New skills are enabled automatically, any chat participant can run /skill install, and the model itself can add an MCP server by editing mcp.json, which is then launched with no consent. MCP stdio servers get a scrubbed environment, but skill scripts run through the bash tool with every API key in their environment.

- **S L0:** Model-chosen code runs automatically: an mcp.json edit by the agent is hot-reloaded and its command launched, and the executable allowlist is empty by default. — [bridge/agent_bridge.py:1888-1890](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/bridge/agent_bridge.py#L1888-L1890); [agent/tools/mcp/mcp_client.py:335-336](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/mcp/mcp_client.py#L335-L336); [agent/tools/mcp/mcp_client.py:275-276](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/mcp/mcp_client.py#L275-L276); [bridge/agent_initializer.py:947](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/bridge/agent_initializer.py#L947) (verified)
  - *To reach the next level:* Extensions limited to user-chosen sources, never model-chosen ones.
- **C L0:** No extension type is verified: skill installs track a branch head and trust a checksum header from the same server; MCP commands are unpinned. — [cli/commands/skill.py:164](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/cli/commands/skill.py#L164); [cli/commands/skill.py:1422-1423](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/cli/commands/skill.py#L1422-L1423); [agent/tools/mcp/mcp_client.py:335-336](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/mcp/mcp_client.py#L335-L336) (verified)
  - *To reach the next level:* Verification (pinning) for at least one extension type.
- **D L0:** Files dropped into the workspace become enabled skills or MCP servers automatically, and the /skill chat command has no admin check. — [agent/skills/manager.py:174](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/skills/manager.py#L174); [bridge/agent_bridge.py:1888-1890](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/bridge/agent_bridge.py#L1888-L1890); [plugins/cow_cli/cow_cli.py:149-152](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/plugins/cow_cli/cow_cli.py#L149-L152) (verified)
  - *To reach the next level:* Explicit consent on first use of any extension.
- **B L0:** Skill scripts execute via the bash tool as the same user with the full environment, including all API keys; only MCP stdio servers receive a scrubbed environment. — [agent/tools/bash/bash.py:167](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L167); [agent/tools/mcp/mcp_client.py:358-366](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/mcp/mcp_client.py#L358-L366) (verified)
  - *To reach the next level:* Every extension in a separate process with a scrubbed environment.
- **Cap:** C7-RCELOAD — By default an agent-written mcp.json entry is hot-reloaded and its command executed without any user consent.

### C8 Secrets & sensitive-data protection — 0.12 (high)

The signing secret used by the web console is not well protected. API keys are stored in plaintext config.json and ~/.cow/.env (the latter chmod 600), then exported to every bash subprocess, and the model is told it can use them as $VARS. The config log line and the bash progress stream are masked, and MCP stdio servers get a scrubbed environment; there is no telemetry by default.

- **S L0:** The signing secret is not well protected; credentials are plaintext and routinely available to model-run commands. — [agent/tools/bash/bash.py:47](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L47) (verified)
  - *To reach the next level:* Hardened secret handling, plus masking on main paths.
- **C L1:** Masking covers the config log line and the bash progress stream; tool results returned to the model and the bash subprocess environment are unprotected. — [config.py:583](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L583); [agent/tools/bash/bash.py:545-556](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L545-L556); [agent/tools/mcp/mcp_client.py:358-366](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/mcp/mcp_client.py#L358-L366) (verified)
  - *To reach the next level:* Protection for logs and transcripts as well, not just progress snapshots.
- **D L1:** No telemetry SDK is present and the cloud client starts only when LinkAI is configured, but redaction is partial and payloads reach logs at debug level unredacted. — [app.py:259-261](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/app.py#L259-L261); searched `rg -n -i "sentry|posthog|telemetry|mixpanel"` in `agent bridge channel common models plugins config.py app.py` → 1 hits (The single hit is webkitGetAsEntry in channel/web/static/js/views/skills.js (substring match on sentry); no telemetry SDK is present.) (verified)
  - *To reach the next level:* Logging defaults that keep payloads out, with redaction always on.
- **B L0:** Long-lived, high-privilege model and chat-platform keys are reachable by the model and by every subprocess. — [agent/tools/bash/bash.py:167](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L167); [config.py:698-702](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L698-L702); [config.py:681](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L681) (verified)
  - *To reach the next level:* Keys not reachable by every subprocess, or scoped keys.
- **Cap:** none

### C9 Audit & traceability — 0.40 (high)

Every run is persisted step by step into a SQLite conversation store with timestamps, tool calls and their results, and run.log records each tool call with its arguments. The store lives inside the agent's own writable workspace (memory/long-term/index.db), persistence is explicitly best-effort, and there is no actor attribution beyond agent and session ids. With no approval system, there is nothing to log on that front either.

- **S L2:** A structured transcript of messages including tool_use and tool_result blocks with created_at timestamps, plus run.log lines naming each tool call and its arguments. — [agent/memory/conversation_store.py:60-70](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/memory/conversation_store.py#L60-L70); [agent/protocol/agent_stream.py:962](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/protocol/agent_stream.py#L962) (verified)
  - *To reach the next level:* Actor attribution (requesting principal, approver) and correlation ids across sub-agents.
- **C L2:** All tool calls in the main loop, MCP tools included, are recorded; sub-agents run with output_mode logger and leave only log lines. — [agent/protocol/step_writer.py:9](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/protocol/step_writer.py#L9); [agent/subagent/runner.py:114](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/subagent/runner.py#L114) (verified)
  - *To reach the next level:* Full records for sub-agents, plus approvals and denials.
- **D L1:** On by default, but stored under the workspace the agent's write and bash tools can modify. — [agent/memory/conversation_store.py:2593](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/memory/conversation_store.py#L2593); [bridge/agent_initializer.py:947](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/bridge/agent_initializer.py#L947) (verified)
  - *To reach the next level:* Records stored outside the agent's writable workspace.
- **B L1:** Steps are written as each turn ends, but failures are logged and swallowed while the run proceeds. — [agent/protocol/step_writer.py:9](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/protocol/step_writer.py#L9) (verified)
  - *To reach the next level:* Errors surfaced to the operator with records flushed per action.
- **Cap:** none

### C10 Limits & kill switch — 0.40 (high)

A run is capped at 30 decision steps by default, each shell command times out after 120 seconds (600 maximum), and a user cancel kills the running command's process group. Sub-agents get half the parent's steps, a 300-second budget and at most three at a time, but there is no token or spend cap and no wall-clock limit on the main run. Background shell jobs keep running after a run ends, scheduled tasks keep firing, and any chat participant can raise agent_max_steps with /config.

- **S L2:** Iteration cap plus per-command timeouts, enforced in code; cancel kills the bash process group. — [config.py:309](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L309); [agent/tools/bash/bash.py:39-40](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L39-L40); [agent/tools/bash/bash.py:539-540](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L539-L540) (verified)
  - *To reach the next level:* A token/cost cap and wall-clock limit on the run, plus rate limits on side-effecting tools.
- **C L2:** Limits cover the top-level loop and tool timeouts; sub-agents get their own fresh (half-size) budget rather than drawing on the parent's. — [agent/subagent/runner.py:120](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/subagent/runner.py#L120); [config.py:322-324](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L322-L324); [agent/tools/bash/bash.py:66](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L66) (verified)
  - *To reach the next level:* Sub-agents, background jobs and scheduled tasks counted against the same budget.
- **D L1:** The defaults are sensible, but the unauthenticated /config chat command and the full-access model (which can edit config.json) can raise them. — [plugins/cow_cli/cow_cli.py:766-776](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/plugins/cow_cli/cow_cli.py#L766-L776); [plugins/cow_cli/cow_cli.py:149-152](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/plugins/cow_cli/cow_cli.py#L149-L152); [config.py:315](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/config.py#L315) (verified)
  - *To reach the next level:* Limits the model and chat participants cannot raise.
- **B L1:** Stopping leaves background jobs and scheduled agent tasks running, and there is no spend ceiling. — [agent/tools/bash/bash.py:66](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/bash/bash.py#L66); [agent/tools/scheduler/scheduler_tool.py:250](https://github.com/zhayujie/CowAgent/blob/2b2d11270ae6472358242e4926fbf1d8f0d21b11/agent/tools/scheduler/scheduler_tool.py#L250) (verified)
  - *To reach the next level:* Moderate ceilings with nothing left running after stop.
- **Cap:** none

## Rule-of-Two check
[A] untrusted input: web_fetch/web_search/browser/files/MCP results/chat messages entered as tool results (agent/protocol/agent_stream.py:446) · [B] sensitive data/systems: all API keys and channel secrets in the bash environment (config.py:698-702, agent/tools/bash/bash.py:167) · [C] state change / egress: bash shell=True on host and arbitrary-URL web_fetch with SSRF guard off (agent/tools/bash/bash.py:456-465, agent/tools/utils/url_safety.py:4-7) · Same default session? Yes

## Highest-impact improvements
1. Harden signing-secret handling for the web console. — C8 S L0→L1, +0.075 before caps (Playbook 4)
2. Stop exporting config secrets into os.environ and give bash and skill subprocesses a scrubbed environment (as MCP stdio already does), injecting only the keys a skill declares. — C1 C L0→L1, +0.075 before caps (Playbook 4)
3. Move mcp.json and other security-relevant settings out of the agent-writable workspace and require a user confirmation in the console before any new MCP server or skill is enabled. — C6 S L0→L2, +0.150 before caps (Playbook 2)
4. Add per-call human approval in the console/channel showing the exact command for bash, out-of-workspace writes, browser actions and outbound sends, on by default. — C2 S L0→L3, +0.225 before caps (Playbook 5)
5. Once untrusted content (web, files, MCP, other chat users) enters a session, force egress and state-changing tools through approval. — C5 S L0→L2, +0.150 before caps (Playbook 1)

## Re-audit log
- No changes.

## Limitations
- Static source review of the pinned commit only; nothing was executed, installed, or probed.
- Scored the README-led one-line installer/source deployment with the web console as the only channel. IM channels (Telegram, Slack, Discord, Feishu, WeChat and others) are opt-in; when enabled they accept instructions from any sender with no allowlist, which would also trigger C5-PUBLICTRIGGER.
- The desktop client (Electron, desktop/) was not reviewed in depth; its first launch defaults to workspace-write (see the C3 alt).
- Browser automation internals, individual model-provider clients, and the LinkAI cloud client were reviewed only at their entry points.
- No reviewer-directed prompt-injection text was found in the repository.
