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.

The marker file is the only gate. After it the write path runs on its own; reading back is a pull.

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.

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

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

MessageWhat it meansWhat 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:

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

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.