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.
Two credentials, and they open different doors
One signs in as you; the other identifies your agent. They are issued separately, revoked separately, and neither works in place of the other.
Yours The password account
Signs you into the web console to 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.
Your agent's The API key
A long string beginning aim_ that identifies your agent to the server.
The join script stores it in two files only you can read. It is shown once and cannot
be retrieved afterwards, and there is exactly one per person per machine: a lost
laptop costs one revocation, not your account.
Marked repositories
Where your agent already works. A two-line marker file is the switch; without it, nothing is sent.
Lifecycle hooks
Small programs in your agent that report events to a queue on your own disk.
The server
Sanitises, stores, and compiles sessions into pages. The only place a model runs.
The memory
You browse it in the web console; your agent queries it on demand.
Join in three steps
Want to read the script first? It is short and holds no secret — open join.sh. Re-running it is safe: every step is idempotent.
-
Get two credentials from whoever sent you this page. A username with 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. -
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 and look for the session; it appears within seconds. The console wants your
password, not the aim_ key. Restart any agent that was running during the
join; Codex asks you to trust its new hooks the next time it starts.
Verified on Linux with an x86_64 processor. The script detects macOS and 64-bit ARM machines and requests the matching build, but nobody here has run those yet; if you are first, tell whoever sent you this page what happened.
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. Pasting the
aim_PASTE-YOUR-KEY placeholder itself is the usual cause. |
Ask whoever issued your key for 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 whoever sent you this page 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 choose what each repository sends
The join installs allowlist capture, and 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
The prompts you type, in full; the families of the tools your agent runs; 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 into your agent: the memory tools and the rules for scoping them. Two files add project-specific policy, and they steer different things.
A project skill steers your agent: 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/
(whoever issued your credentials can grant access).
A wiki page steers the server-side compiler: a
_prompts/consolidation.md page in the project's wiki tells the model that
compiles sessions into pages what is worth keeping, where it is filed, and what must never
be recorded.
On the engineering instance, nothing is reviewed before it lands. A page your agent writes is live and searchable immediately — and so is a delete. Correction happens after the fact: 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.
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, backing up every
file it edits; restart your agents afterwards. --agent unwires one agent, and
--purge also deletes the client binary, the hook bundle, and queued events.
Without jq installed, the script tells you which files to edit by hand.
One person: revoke the key
Whoever issued the credentials revokes the key, or disables the account for the console; any client that still fires then authenticates to nothing. Upstream ships no uninstall of its own (the installers write but never remove), so the reversal is the join script's.
For whoever issues the credentials
One command creates the account, issues the key, and prints everything the person needs,
labelled. Whoever sent you this page administers the server: anyone with Azure access to
this deployment. The command runs against the virtual machine through Azure: no secure-shell
login, and no shared secret leaves the key vault. You need the az CLI signed in
to the IFS Copperleaf Cloud tenant (the VMs live in the sub-strategy-sandbox
subscription, which the command names explicitly) and jq locally. The example
joins Jane Doe and labels the key for her laptop; change the username, email, name, and
label:
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 "echo '=== username: jdoe ===' && \
echo '=== temporary web password (shown once; change on first login) ===' && \
docker exec cl-memory-ai-memory-1 ai-memory user add-human \
--username jdoe --email jdoe@copperleaf.cloud --name 'Jane Doe' && \
echo '=== aim_ API key (shown once; label jane-laptop) ===' && \
docker exec cl-memory-ai-memory-1 ai-memory api-key add \
--username jdoe --label jane-laptop" \
-o json | jq -r '.value[0].message'
- --username jdoe
- the person's login name; the key command reuses it.
- --email, --name
- their email and display name, quoted.
- --label jane-laptop
- a name for the machine the key will live on, so you recognise it later when revoking. One key per label.
The echo markers label the output for forwarding. The filter at the end prints
the script's output: the invoke response carries it as one
ProvisioningState/succeeded entry, with stdout and stderr inside
.value[0].message between [stdout] and [stderr]
markers — there are no per-stream entries to filter for. Do not swap the filter for
something cleverer and do not truncate what it prints: the response is the only copy anyone
will ever hold of the two secrets below.
The response gives you, in order, everything to send the person:
=== username: jdoe ===— the login name.=== temporary web password … ===— a password for the web console athttps://cl-memory-eng.westus3.cloudapp.azure.com/web; the sign-in form makes them replace it on first use.=== aim_ API key … ===— theaim_key their agent uses. One key per person per machine, so a lost laptop is one revocation.
Send the password and the key by separate channels. Neither is recoverable from the server
afterwards, so if the output is lost anyway, do not re-run the command:
add-human fails on an existing username and api-key add mints a
second credential. Recover against the same VM, then send the replacements:
# Find the credential's ID by its label; metadata only, no secrets.
az vm run-command invoke … --scripts "docker exec cl-memory-ai-memory-1 ai-memory api-key list"
# Rotate it: the lost plaintext 401s immediately and a replacement prints once.
az vm run-command invoke … --scripts "docker exec cl-memory-ai-memory-1 ai-memory api-key rotate <ID> --yes"
# Reset the web-console password: revokes sessions, prints a temporary password once,
# and forces a change on next login.
az vm run-command invoke … --scripts "docker exec cl-memory-ai-memory-1 ai-memory user reset-password <username> --yes"
Both recovery commands need --yes: run-command provides no stdin,
so an interactive confirmation aborts the command.
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: sanitises, 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
What leaves your machine, event by event
Each hook receives your harness's event and forwards it; the server 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.
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. Registering the server is usually enough on its own, because the connection handshake
ships its own routing instructions. 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. 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, and server registration for
those plus Claude Desktop, VS Code Copilot, Zed, Swival, and Muse. None 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. 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
- 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 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.