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

  1. 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.
  2. 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 sh

    The script checks the key against the server before it changes anything, then wires Claude Code, Codex, Cursor, and OpenCode (whichever it finds). curl is the only requirement: no Azure access, no administrator credential, no copy of the deployment repository.

  3. Opt in the repositories you want remembered. Commit two lines as .ai-memory.toml at 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

MessageWhat it meansWhat 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
      
The write path runs top to bottom on its own. The read path is the one arrow on the right: your agent asks, and only then. Only the blue box is a model, and only the server calls it.

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.

EventWhat the hook sendsWhat 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:

  1. Hooks make no model call. They are a small program posting JSON over HTTPS.
  2. 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.
  3. 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.