Files
mempalace/hooks

MemPalace Hooks — Auto-Save for Terminal AI Tools

These hook scripts make MemPalace save automatically. No manual "save" commands needed.

This deployment ships only the remote hook variants — the palace runs as a Docker container on a server (e.g. Unraid), and hooks curl the active session transcript to the server's /ingest/transcript endpoint over HTTPS with bearer auth. Server-side, the existing mine_convos pipeline handles entity detection, room assignment, dedup, and idempotency. See deploy/unraid/README.md for the server side.

What They Do

Hook When It Fires What Happens
Save Hook (mempal_save_hook_remote.sh) Every 15 user messages (configurable via SAVE_INTERVAL) Backgrounded curl POSTs the active transcript. Returns immediately so the AI doesn't stall. Idempotent — failed retries are safe.
PreCompact Hook (mempal_precompact_hook_remote.sh) Right before context compaction Synchronous curl POST. Blocks until the upload completes (or the hook timeout fires) so memory is durable before context shrinks.

Two-layer capture. The save hook ships the JSONL transcript directly to the server (capturing raw tool output — Bash results, search findings, build errors), where the miner files it verbatim into the palace. Tool output gets stored even if the AI summarizes instead of quoting.

Env-var contract

The scripts read all configuration from environment variables. There is no script-level config to edit; the same script works against any number of machines.

Variable Required Purpose
MEMPAL_REMOTE_URL yes Base URL of the MemPalace server, e.g. https://unraid.local:8443.
MEMPAL_REMOTE_TOKEN yes Bearer token shared with the server's MEMPAL_TOKEN.
MEMPAL_REMOTE_INSECURE no Set to 1 to skip TLS verification. Use only when the server uses Caddy's tls internal self-signed cert and the client hasn't trusted the root CA.
MEMPAL_REMOTE_WING no Force a specific wing for this client's transcripts. Default: server derives wing from the session id.
SAVE_INTERVAL no Override the default of 15 user messages.
MEMPAL_PYTHON no Path to a Python 3 interpreter. Only needs json + sys from stdlib — mempalace does not need to be installed in it. Used to parse the hook's stdin JSON.

Set these persistently:

PowerShell (Windows):

[Environment]::SetEnvironmentVariable("MEMPAL_REMOTE_URL",   "https://unraid.local:8443", "User")
[Environment]::SetEnvironmentVariable("MEMPAL_REMOTE_TOKEN", "<the-token>",               "User")
[Environment]::SetEnvironmentVariable("MEMPAL_REMOTE_INSECURE", "1", "User")  # if self-signed

Bash/Zsh: add the same exports to ~/.zshrc / ~/.bashrc.

If MEMPAL_REMOTE_URL or MEMPAL_REMOTE_TOKEN is unset, the scripts no-op and log a one-liner — they never block the AI from stopping. Safe to install on a machine that doesn't have a remote configured yet.

Install — Claude Code

Make the scripts executable:

chmod +x hooks/mempal_save_hook_remote.sh hooks/mempal_precompact_hook_remote.sh

Add to .claude/settings.local.json:

{
  "hooks": {
    "Stop": [{
      "matcher": "*",
      "hooks": [{
        "type": "command",
        "command": "/absolute/path/to/hooks/mempal_save_hook_remote.sh",
        "timeout": 30
      }]
    }],
    "PreCompact": [{
      "hooks": [{
        "type": "command",
        "command": "/absolute/path/to/hooks/mempal_precompact_hook_remote.sh",
        "timeout": 60
      }]
    }]
  }
}

Install — Codex CLI (OpenAI)

Add to .codex/hooks.json:

{
  "Stop": [{
    "type": "command",
    "command": "/absolute/path/to/hooks/mempal_save_hook_remote.sh",
    "timeout": 30
  }],
  "PreCompact": [{
    "type": "command",
    "command": "/absolute/path/to/hooks/mempal_precompact_hook_remote.sh",
    "timeout": 60
  }]
}

How it works

Save Hook (Stop event)

User sends message → AI responds → Claude Code fires Stop hook
                                            ↓
                                    Hook counts user messages in JSONL transcript
                                            ↓
                              ┌─── < SAVE_INTERVAL since last save ──→ echo "{}" (let AI stop)
                              │
                              └─── ≥ SAVE_INTERVAL since last save
                                            ↓
                                    Background curl POST → server /ingest/transcript
                                            ↓
                                    Hook returns {} immediately (AI stops normally)
                                            ↓
                                    Server-side miner runs in background, files drawers

PreCompact Hook

Context window getting full → Claude Code fires PreCompact
                                        ↓
                                Synchronous curl POST → server /ingest/transcript
                                        ↓
                                Wait for 200 OK (or hook timeout)
                                        ↓
                                echo "{}" → Compaction proceeds

Synchronous on PreCompact is intentional — this is the safety net before context shrinks. The Claude Code hook timeout (set in settings.local.json) bounds how long we'll wait.

Debugging

tail -f ~/.mempalace/hook_state/hook.log

Example:

[14:30:15] Session abc123: 12 exchanges, 12 since last save
[14:35:22] Session abc123: 15 exchanges, 15 since last save
[14:35:22] ingest ok
[14:50:18] PRE-COMPACT triggered for session abc123
[14:50:19] PRE-COMPACT ingest ok

A 401 response means the bearer token is wrong. A connection error means the URL/cert is wrong (or the server is down). All curl output goes to the same log.

Known limitations

Hooks require session restart after install. Claude Code loads hooks from settings.json at session start only. If you edit hook config mid-session, restart Claude Code to pick up changes.

Python interpreter resolution. The scripts parse hook stdin JSON with python3. When Claude Code is launched from a GUI on macOS (Spotlight, dock, open -a), its PATH is the minimal /usr/bin:/bin:/usr/sbin:/sbin inherited from launchd rather than your shell PATH. If python3 isn't there, set MEMPAL_PYTHON to a known-good interpreter:

export MEMPAL_PYTHON="/usr/bin/python3"
# or:
export MEMPAL_PYTHON="$HOME/.venvs/x/bin/python"

Resolution priority: $MEMPAL_PYTHON$(command -v python3) → bare python3. The interpreter only needs json and sys — mempalace itself does not need to be installed.

MineAlreadyRunning collisions. If two clients ingest simultaneously, the second one's request returns 500 because the server-side mine_lock is held. The save hook is idempotent — the next save catches up. If you see this constantly in the log, raise SAVE_INTERVAL on the chattier client.

Backfilling past conversations

The hooks only capture sessions going forward. To mine past sessions into the remote palace, loop curl over them:

# Claude Code sessions
for f in ~/.claude/projects/**/*.jsonl; do
  curl -k -X POST \
    -H "Authorization: Bearer $MEMPAL_REMOTE_TOKEN" \
    -H "X-Session-Id: $(basename "$f" .jsonl)" \
    --data-binary @"$f" \
    "$MEMPAL_REMOTE_URL/ingest/transcript"
done

# Codex CLI sessions
for f in ~/.codex/sessions/**/*.jsonl; do
  curl -k -X POST \
    -H "Authorization: Bearer $MEMPAL_REMOTE_TOKEN" \
    -H "X-Session-Id: $(basename "$f" .jsonl)" \
    --data-binary @"$f" \
    "$MEMPAL_REMOTE_URL/ingest/transcript"
done

The server-side miner is idempotent — re-uploading the same transcript won't double-file. Drop -k once Caddy's root CA is trusted on the client.

Cost

Zero extra tokens. The hooks save in the background — the AI doesn't need to write anything in the chat window. All filing happens server-side after the upload returns.