Join Copperleaf Memory
A shared server that remembers what coding agents learn about a codebase, so the next session starts informed. Joining takes two credentials and one command, and nothing is sent from a repository until you opt that repository in.
Join in three steps
-
Get two credentials from whoever sent you this page. That person
administers the server. You need a username and a temporary password, which sign
you into the web console, and an
aim_API key, which your agent uses. Nobody has issued yours yet? It is one command. -
Join from any terminal. Replace aim_PASTE-YOUR-KEY
with the key you were sent,
aim_included:curl -fsSL \ https://cl-memory-eng.westus3.cloudapp.azure.com/join.sh \ | AI_MEMORY_JOIN_TOKEN=aim_PASTE-YOUR-KEY shThe script checks the key against the server before it changes anything, then wires Claude Code, Codex, Cursor, and OpenCode (whichever it finds).
curlis the only requirement: no Azure access, no administrator credential, no copy of the deployment repository. -
Opt in the repositories you want remembered. Commit two lines as
.ai-memory.tomlat each repository's root:workspace = "default" project = "my-project"A repository without this file never sends anything. Not reduced, not anonymised: nothing. What a marked repository sends is below.
Check it worked: start a session in a marked repository, then open the web
console at https://cl-memory-eng.westus3.cloudapp.azure.com/web and look for
the session — it appears within seconds. The console asks for your password account, not
the aim_ key. Events arrive in batches as a local queue drains, and the page
that summarises the session is written when the session ends.
Read the script before you run it
It is a short shell script and it holds no secret:
open join.sh,
or download the copy published beside this page and run
sh join.sh --token aim_PASTE-YOUR-KEY.
Re-running is safe: every step is idempotent, an unchanged install reports that it changed
nothing, and each installer backs up the file it rewrites. Three options are worth knowing.
--agent claude-code wires one agent instead of all of them, and repeats.
--scope project confines Claude Code to the checkout you are standing in rather
than the whole machine. --server-url points at a different instance.
Verified on Linux with an x86_64 processor only. The script detects macOS and 64-bit ARM machines and requests the matching build, but no one has run it on those yet. Tell the operator what happens if you are first. Codex also asks you to trust its new hooks the next time it starts — choose "Trust all and continue".
If the join fails
| Message | What it means | What to do |
|---|---|---|
| the server refused this key (401) | The key is wrong, revoked, or was copied with a character missing. | Ask the operator to issue a new one. |
| checksum mismatch | The downloaded client does not match the checksum the upstream project published. | Run it again. If it repeats, stop and tell the operator rather than bypassing it. |
| found none of: claude-code codex cursor open-code | None of the four agents the command knows about is installed, or none is where it looks. | Install one and run the join again, or name yours with --agent. |
| run --scope project from inside a git checkout | You passed --scope project from a directory that is not a git
repository. |
Change into the repository you want remembered, or drop the option. |
You get two credentials, and they open different doors
One signs in as you; the other identifies your agent. They are issued separately, they are revoked separately, and neither works in place of the other.
The password account is yours
It signs you into the web console, where you can read and search everything the instance remembers. It reaches you as a temporary password, and the sign-in form makes you replace it before letting you in. Your agent never touches it — an agent has no password and no use for one.
The API key is your agent's
A long string beginning aim_, it identifies you on the server's machine
interface, the channel your agent and its lifecycle
hooks use. The join script stores it in two files that only you can read; it never lands
in an agent config. It is shown once and cannot be retrieved afterwards, and there is
exactly one per person per machine, so a lost laptop costs one revocation rather than your
account.
A repository sends nothing until it carries a marker file
The join installs allowlist capture. The marker file from step 3 is the whole switch:
No marker Nothing, ever
Every other repository on your machine. Your agent's sessions there produce no event at all. The work is not merely unattributed, it is never sent.
Marker present Session activity, attributed to you
Session events, the prompts you type in full, the families of the tools your agent runs, and capped excerpts of what those tools printed. Your agent's replies are not sent. The field-by-field detail is below.
So you join once and still choose, repository by repository, what the server sees. One limit to know: everyone with an account can read every page on the instance — there is no per-page permission.
Steer what gets remembered
The join wires generic routing: your agent sees the memory tools and the rules for scoping
them, either from the server's connection handshake alone or, if you run
ai-memory install-instructions, from a marked block in the repository's
CLAUDE.md or AGENTS.md. Two files add project-specific policy, and
they steer different things.
A project skill steers your agent
It tells the agent when to reach for memory: search before designing, rate a page the moment
it proves wrong, write a durable page when a fact settles, hand off at wrap-up. Copy the
template from the deployment repository's .agents/skills/memory/ into your
project's skills directory — ask the operator for access if you do not have it. It sits
outside the ai-memory- namespace and carries no managed marker, so the vendor's
installer leaves it alone.
A wiki page steers the server-side compiler
A _prompts/consolidation.md page in the project's wiki steers the model that
compiles sessions into pages: what is worth keeping, where it is filed, what must never be
recorded. It is the only per-project steering the server's scheduled reviewer sees, because
that run happens with no agent in the loop. ai-memory treats the page as bounded, sanitized,
untrusted advice — it shifts emphasis and cannot supply facts.
On the engineering instance, nothing is reviewed before it lands. A page your agent writes is live and searchable immediately, a delete is equally ungated, and the scheduled reviewer promotes its own proposals unattended. That is deliberate: a queue nobody drains is a reviewer that never ran. Correction happens after the fact instead: rate a bad page, let the daily lint surface it, delete it if it should not be there.
Turning it off — a repository, a machine, or a person
None of these deletes what was already sent. Compiled pages age out by retention.
One repository: delete its marker
Remove the repository's .ai-memory.toml. The hooks stay installed, but the
repository emits nothing, immediately, with no restart and no uninstall. This is the
per-repository opt-out allowlist capture exists for.
One machine: uninstall
curl -fsSL https://cl-memory-eng.westus3.cloudapp.azure.com/join.sh | sh -s --uninstall
Needs no key and never contacts the server. It removes the hook entries, the MCP
registration, and the OpenCode plugin from each agent's own configuration, backs up every
file it edits, and touches nothing else in those files. Restart your agents afterwards.
--agent unwires one agent; add --purge to also delete the client
binary, the hook bundle, and the data directory, queued events included. Without
jq installed, the script tells you which files to edit by hand.
One person: revoke the key
An operator revokes the person's key, or disables the account for the console. Any client that still fires then authenticates to nothing. Upstream 2.2.1 ships no uninstall of its own — the installers write but never remove — so the reversal is the join script's, not the vendor's.
For whoever issues the credentials
An operator is anyone with Azure access to this deployment — the person who set it up, or
whoever sent you this page. Creating a person's account and their first key is one command,
run against the virtual machine through Azure, so it needs no secure-shell login and no
shared secret leaves the key vault. Sign in to the IFS Copperleaf Cloud tenant first; the
VMs live in the sub-strategy-sandbox subscription, which the command names
explicitly. The example below joins Jane Doe and labels the key for her laptop:
az vm run-command invoke \
--subscription c074d2e3-9d2e-456e-8709-cc199c95a66f \
-g rg-cl-memory-eng -n cl-memory-eng-vm --command-id RunShellScript \
--scripts "docker exec cl-memory-ai-memory-1 ai-memory user add-human \
--username jdoe --email jdoe@copperleaf.cloud --name 'Jane Doe' && \
docker exec cl-memory-ai-memory-1 ai-memory api-key add \
--username jdoe --label jane-laptop"
- --username jdoe
- the person's login name; the second command reuses it.
- --email, --name
- their email and display name, quoted.
- --label jane-laptop
- a name for the machine the key will live on; any string you will recognise later when revoking. One key per label.
The command prints the temporary password and the aim_ key, each exactly once;
the server cannot show either again. Send them to the person by separate channels.
How it works, for the reader who wants the mechanism
What leaves your machine, who does the work, and where the model runs. Every figure here was measured against the engineering instance on 2026-09-15.
flowchart TD
harness["Your harness: prompts and tool calls"]:::agent --> hook["Lifecycle hook, returns in about 10 ms"]:::client
hook --> spool[("Local queue on your own disk")]:::client
spool -->|"drained in batches"| srv["Server: sanitizes, bounds, stores"]:::svc
srv --> obs[("Observations, the evidence layer")]:::store
obs -->|"compiled by rule, no model"| sess["Session pages"]:::svc
obs -->|"server's own key"| llm["gpt-5.6-luna, consolidation"]:::model
llm -->|"staged for an approver"| pages[("Compiled pages, the knowledge layer")]:::store
agent2["Your agent, only when it asks"]:::agent -->|"query and read"| pages
classDef agent fill:#fdf3d8,stroke:#d97706,color:#7c4a03
classDef client fill:#e9eaed,stroke:#6c757d,color:#495057
classDef svc fill:#e6f4ea,stroke:#1a7f37,color:#14602a
classDef model fill:#e5edff,stroke:#2563eb,color:#1e3a8a
classDef store fill:#ffffff,stroke:#1f2430,color:#1f2430
What leaves your machine, event by event
Each hook receives your harness's event and forwards it. The server decides what to keep, and it keeps considerably less than it is given.
| Event | What the hook sends | What the server stores |
|---|---|---|
| session start | session id, working directory, transcript path | an empty-bodied record; the reply carries the pending handoff |
| your prompt | the prompt text | the prompt, verbatim |
| before a tool runs | tool name, arguments, call id | the tool family (file, non-file,
unknown) and the call id — not the command, not the
arguments |
| after a tool runs | tool name, arguments, result, duration | family, call id, outcome, and the tool's output, capped near 2,000 characters |
| end of a turn, end of a session | the harness's event | an empty-bodied record marking the boundary |
Your prompts are kept in full and everyone with an account can read them.
Your agent's replies are not kept at all — no assistant message appears in any stored
record, and the hook bundle discards an end-of-turn event outright if it still carries one.
Credential shapes are stripped before storage: in the records captured on 2026-09-15,
session cookies read REDACTED and API keys read
[REDACTED:meta_token].
Delivery is queued rather than synchronous. Each hook writes its event to
~/.local/share/ai-memory/hook-spool and returns in about ten milliseconds, so
your agent never waits on the network; a drainer flushes the backlog in batches, behind a
successful request and at session end. A slow link, a server restart, or a flight costs
latency, not events.
Who spends tokens
Nobody's coding agent. Three claims, each of which you can check:
- Hooks make no model call. They are a small program posting JSON over HTTPS.
- Session pages are compiled by rule, not by a model. Every session page
ends
Synthesised by ai-memory (M3, no-LLM heuristic)and records the process, not a model, as its author. - The model runs on the server, on the server's own key. The one-off bootstrap of this repository sent about 18,300 input tokens in a single request and wrote the concept, decision, and gotcha pages. Scheduled consolidation uses the same provider, and the engineering instance requires an administrator to approve a promotion before it enters the wiki.
Your agent spends tokens only on what it asks for: the handoff injected at session start, and whichever pages it queries and reads.
How your agent knows to use the memories
Retrieval is a pull. Nothing enters your context automatically except the handoff at session start; everything else happens because your agent decided to call a tool.
Registering the server is usually enough on its own — it ships its own routing instructions
in the connection handshake, so a harness that speaks the Model Context Protocol shows the
agent both the tools and the rules for scoping them, with no file in your repository. When you
want instructions the repository can see and review, ai-memory install-instructions
writes a marked block into CLAUDE.md or AGENTS.md and installs
matching Agent Skills.
Keep those instructions at project scope. Installed globally they would tell an agent to search Copperleaf Memory in repositories that never opted in. The marker already keeps those repositories silent, so there would be nothing to find.
Harnesses other than Claude Code
The join command wires Claude Code, Codex, Cursor, and OpenCode — whichever it finds — and
--agent names one explicitly. Each gets the same two pieces: lifecycle hooks for
the write path, server registration for the read path.
The underlying client knows more than those four. It renders lifecycle hooks for Gemini CLI, Kiro, Grok, Devin, Kimi Code, Antigravity, Command Code, Pool, Zero, ZCode, OpenClaw, Pi, and Oh My Pi as well, and server registration for those plus Claude Desktop, VS Code Copilot, Zed, Swival, and Muse. None of those is wired by the join command, because nobody here runs one yet; each is two commands when someone does:
ai-memory install-hooks --agent gemini-cli --apply \
--server-url https://cl-memory-eng.westus3.cloudapp.azure.com \
--auth-token aim_PASTE-YOUR-KEY --capture-mode allowlist \
--hooks-dir "$HOME/.local/share/ai-memory-hooks"
ai-memory install-mcp --client gemini-cli --apply \
--server-url https://cl-memory-eng.westus3.cloudapp.azure.com \
--auth-token aim_PASTE-YOUR-KEY
Two differences to expect. Cursor and OpenCode keep their configuration per user rather than per repository, and allowlist capture is what makes that safe: without a marker the event is dropped before it reaches the local queue, whatever the configuration scope. And Grok, Zero, and Pool discard whatever a session-start hook prints, so they capture normally but cannot receive a handoff; ask the agent for it instead.
Why the console shows so much session data
The store has two layers. Evidence is sessions and their raw records. Knowledge is the compiled pages — concepts, decisions, gotchas. On 2026-09-15 the engineering instance held 6 sessions, 1,153 records, and 14 pages. The session list is long because it is a log; the pages are the part meant to be read.
The volume is smaller than it looks. A stored record runs about 700 bytes and is capped near 2,000, so a two-hour session of 353 records is well under a megabyte against a 32 GiB disk. Compiled pages decay when they go cold, while pinned and semantic pages are exempt; raw records are kept indefinitely under the current settings. Trimming them is a retention setting, not a change to what is captured.
The boundary all of this crosses
Every request above is HTTPS to a virtual machine whose network rule currently admits the whole internet on port 443, so the service's own authentication is the entire perimeter. Both instances are meant to sit behind AppGate; that change is one network parameter and a redeploy, and it is the step that turns a credential-only perimeter into a network one.
Glossary, in plain words
- Operator
- Anyone with Azure access to this deployment. The operator creates accounts, issues and revokes keys, and runs the occasional maintenance command against the VM. See For whoever issues the credentials.
- Lifecycle hooks
- Small commands your agent runs by itself when a session starts, when you submit a prompt, when a tool runs, and when the session ends. On this page they are what sends activity to Copperleaf Memory, and the join command installs them for every agent it finds.
- Instance
- One running server with its own separate store. Copperleaf runs two: an engineering instance for coding-agent memory, which is the one this page joins, and a discovery instance for product research.
- Workspace and project
- The two names that decide where your sessions are filed. The marker file in a repository sets both, which is why two checkouts of the same repository land in the same place.
- Observation
- One stored lifecycle event: a prompt you typed, or the family and capped output of a tool your agent ran. Observations are the evidence layer, and the console lists them under the session they belong to.
- Consolidation
- The server-side pass that reads observations and proposes durable pages. It is the only part of the system that calls a language model, it runs on the server's own credential, and on the engineering instance an administrator approves what it proposes.
- Web console
- The browser view of the store: a page tree per project, full-text search, and the sessions your agents recorded. It is read-only, and it needs a password account.