An OpenClaw agent that monitors Slack, ClickUp, and Google Drive for a single client project. It runs on a 30-minute heartbeat, detects unanswered messages, flags missing tasks, due-date risks, ambiguities, and Sentry events — then proposes concrete actions in Slack for human confirmation. It can create tasks and spawn claude code through ACP.
Two Docker containers on one bridge network (openclaw-net):
| Container | Role | Exposed |
|---|---|---|
openclaw-gateway |
Custom image (built from gateway/Dockerfile on top of ghcr.io/openclaw/openclaw) bundling the OpenClaw gateway and the Claude Code CLI. ACP launches claude as a local stdio subprocess of the gateway — no docker exec, no docker socket. |
127.0.0.1:18789 (SSH tunnel only) |
api-proxy |
FastAPI app. Holds third-party credentials (ClickUp, Slack, Google) and exposes them to the gateway over the internal network. | Internal only (expose: 8000) |
The two containers are credential-isolated: the gateway never sees third-party API tokens, and the proxy never sees Anthropic/Slack bot tokens or GH_TOKEN. See CLAUDE.md for the full isolation model.
The proxy serves one project per deployment. The non-secret shape (ClickUp list ids, Slack channel allowlist, team id) lives in project-config.json at the repo root — gitignored, mounted into the container.
The gateway image bundles a full coding toolchain so the agent can do real engineering work in the team's repos, not just answer questions:
- Claude Code CLI (
@anthropic-ai/claude-code) — launched as a local stdio subprocess by ACP (acpx-config.json). Same process tree as the gateway, nodocker execmux. - GitHub CLI (
gh) — for opening pull requests, reading issues/PR comments, and inspecting CI status. Authenticated viaGH_TOKEN/GITHUB_TOKENfrom.env.openclaw. - Git, ripgrep, fd-find, python3, uv, ruff, mypy — baked into
gateway/Dockerfileso the agent has the basics without bootstrapping per session.
The host directory ./repos/ is bind-mounted into the container at /workspace/repos. The agent works the repositories there and creates per-session git worktrees under /tmp/sessions/$SESSION_ID so concurrent sessions don't step on each other's branches. The cwd: /workspace/repos in openclaw.json resolves to this mount.
./repos/ is gitignored — it's a working area, not committed content.
Once a repo is cloned into ./repos/<name>, the agent can:
- Create a branch in an isolated worktree.
- Make and commit changes.
- Push with
git push(usesGH_TOKEN). - Open a PR via
gh pr createagainst the repo's default branch. - Read review comments via
gh pr view/gh apiand iterate.
The agent never force-pushes, never pushes to main/master, and never merges its own PRs — those are explicit red lines in gateway/agent-rules.md.
Two layers, in priority order:
- Repo-specific
CLAUDE.md(highest priority) — branch naming, commit style, test commands, pre-PR checklist. Lives inside each cloned repo. The agent reads it at session start. - Global
gateway/agent-rules.md— repo-agnostic operating rules (worktree isolation, absolute restrictions, PR flow). Bind-mounted read-only over/home/node/.claude/CLAUDE.mdin the container.
To change global rules, edit gateway/agent-rules.md on the host and docker compose --env-file .env.openclaw restart openclaw-gateway — no rebuild needed.
To change per-repo rules, commit a CLAUDE.md to that repository.
- Docker Engine and Docker Compose v2
- SSH access to the VM
- An Anthropic API key (required even if you plan to use a Team/Pro plan via OAuth — see step 7)
- Slack bot + app tokens, ClickUp token, Google service account key
- A GitHub PAT for the agent's git+gh workflow. Scopes:
repo(clone/push private repos),workflow(trigger CI), andread:orgif your repos live under an org. Classic PAT or a fine-grained token with equivalent permissions on the target repos.
cp .env.openclaw.example .env.openclaw
cp .env.api.example .env.apiFill in real values. Generate secrets:
openssl rand -hex 32 # → OPENCLAW_GATEWAY_TOKEN in .env.openclaw
openssl rand -hex 32 # → API_PROXY_KEY in both .env.openclaw AND .env.api (same value)GH_TOKEN / GITHUB_TOKEN go in .env.openclaw — they're used by the claude-code agent (which now runs in the same container as the gateway) to clone, push, and open PRs.
cp project-config.example.json project-config.jsonEdit project-config.json and fill in:
clickup.lists— map of list name → ClickUp list id (at minimum thebackloglist used for creating tasks)clickup.team_id— ClickUp workspace idclickup.default_assignees— optional default assignees for created tasksclickup.custom_id_prefix— optional. If your ClickUp workspace uses custom task IDs (e.g.PROJ-1234), set this to the prefix (e.g."PROJ") so the proxy passescustom_task_ids=truefor matching ids. Leavenullto always use native ClickUp ids.slack.channels— map of channel name → Slack channel id for every channel the agent should read
This file is gitignored. Do not put secrets in it — tokens live in .env.api.
Drop your service account JSON at ./google-key.json for Google Drive integration (or adjust the mount in docker-compose.yml and the GOOGLE_KEY_PATH value in .env.api if you rename it).
OpenClaw doesn't support ${VAR} substitution for the Slack channel allowlist in openclaw.json. Edit openclaw/openclaw.json (around line 39) and set the channel ID where the agent should respond to @mentions:
"channels": {
"<alma-slack-channel-id>": {
"enabled": true,
"requireMention": true
}
}Only this channels block is hardcoded — other ${SLACK_CHANNEL_ID} references in the same file (bindings, heartbeat) are resolved from the container's environment at runtime, so leave those as-is.
cd repos
git clone <repo_url>This host directory is bind-mounted into the gateway at /workspace/repos and is where the agent works the repos and creates per-session worktrees.
The agent prompts in openclaw/workspace/ define all project-specific agent
behavior — the product description, who the team and client are, how to assign
tasks, what the cadence looks like, the workspace's Slack slug, and so on. The
files committed to this repo are a template with dummy data; you must edit
them before going live or the agent will respond using the placeholders.
| File | What lives here | Must-customize |
|---|---|---|
AGENTS.md |
Operating manual: modes, memory split, red lines, task creation guidelines | "What this project is" section (one paragraph describing your product); example wiki entity / concept lists |
USER.md |
People (DM, client, team), ClickUp + Slack IDs, cadence, Slack workspace slug, assignment heuristics | Everything — names, emails, ClickUp IDs, Slack user IDs, workspace slug |
MEMORY.md |
Curated long-term memory: deliveries, client decisions, known bugs, team patterns | Replace template entries with real seed context (the agent will keep this updated during heartbeats) |
IDENTITY.md |
Agent identity (name, Slack handle) | Slack handle if you renamed the bot from @alma |
SOUL.md |
Voice, source-citation rule, propose-never-decide doctrine | Usually leave as-is; tweak language defaults if needed |
TOOLS.md |
API proxy endpoint reference + memory-wiki tool reference | Usually leave as-is; reflects the api-proxy surface |
HEARTBEAT.md |
Per-cycle playbook (every 30 min) | Usually leave as-is; tweak triggers / thresholds if your project needs different ones |
Minimum to do before the first heartbeat:
- Open
openclaw/workspace/USER.mdand replace every dummy name, email, ClickUp ID, Slack user ID, and theyour-workspaceSlack slug with your real values. The agent uses the Slack-id lists to detect unanswered client messages — if these are wrong, that trigger fires incorrectly. - Open
openclaw/workspace/AGENTS.mdand replace the "What this project is" placeholder paragraph with a real description of the product. - Optionally seed
openclaw/workspace/MEMORY.mdwith starting context (the agent keeps it updated during heartbeats — fine to leave the template at first and let it fill in over time). - The hardcoded Slack channel ID in
openclaw/openclaw.json(step 4 above) must be the channel where the agent receives @mentions.
After editing workspace files, restart the gateway:
docker compose --env-file .env.openclaw restart openclaw-gateway(Workspace files are bind-mounted, so a restart is enough — no rebuild needed.)
docker compose --env-file .env.openclaw up -d --buildThe first run builds the custom gateway image (Dockerfile in gateway/); subsequent runs reuse the cached image until you rebuild.
There are two independent Anthropic consumers inside openclaw-gateway:
- OpenClaw's
mainagent (heartbeat, Slack-driven sessions) — usesANTHROPIC_API_KEYfrom.env.openclaw. Always required. - The
claudesubprocess that ACP launches for coding sessions — should use OAuth credentials from a Pro/Team plan to avoid burning API credits on long edits.
If you have a Pro/Team plan, attach it once after the gateway is up:
docker compose --env-file .env.openclaw exec -it openclaw-gateway claude /loginOAuth credentials persist in the claude-home named volume across rebuilds. Do not remove ANTHROPIC_API_KEY from .env.openclaw — OpenClaw's heartbeat agent still needs it.
Why this works: when both are present,
claudenormally prefersANTHROPIC_API_KEYover OAuth.acpx-config.jsonlaunches the subprocess withenv -u ANTHROPIC_API_KEY claude ...to strip the variable for that one process, so the heartbeat keeps using the API key while the coding subprocess uses OAuth.
Verify the subprocess is actually using OAuth:
docker compose --env-file .env.openclaw exec openclaw-gateway \
sh -c 'env -u ANTHROPIC_API_KEY claude --print "/status"'Expect apiKeySource: "claude.ai" (or similar) and a non-null email. If you see apiKeySource: "ANTHROPIC_API_KEY", OAuth didn't take — re-run /login.
docker compose --env-file .env.openclaw exec openclaw-gateway openclaw wiki init
docker compose --env-file .env.openclaw exec openclaw-gateway openclaw wiki statusOptionally seed the wiki with initial project context:
docker compose --env-file .env.openclaw exec openclaw-gateway \
openclaw wiki apply synthesis "Project Overview" \
--body "<one or two sentences describing your product, the team building it, and the development stage>"
docker compose --env-file .env.openclaw exec openclaw-gateway openclaw wiki compileThe agent will populate the wiki automatically during heartbeats. Manual seeding is optional.
# Gateway → api-proxy connectivity
docker compose --env-file .env.openclaw exec openclaw-gateway curl http://api-proxy:8000/health
# Authenticated endpoint
docker compose --env-file .env.openclaw exec openclaw-gateway \
sh -c 'curl -H "X-API-Key: $API_PROXY_KEY" http://api-proxy:8000/protected'Confirm credential isolation between the two containers — third-party tokens must not leak into the gateway, and gateway/agent secrets must not leak into the proxy:
# Should be EMPTY — gateway/agent secrets must not reach api-proxy
docker compose --env-file .env.openclaw exec api-proxy env \
| grep -E 'SLACK_BOT|SLACK_APP|ANTHROPIC|GH_TOKEN|GITHUB_TOKEN'
# Should be EMPTY — third-party creds must not reach the gateway
docker compose --env-file .env.openclaw exec openclaw-gateway env \
| grep -E 'CLICKUP_TOKEN|SLACK_TOKEN|GOOGLE_KEY_PATH|SENTRY_TOKEN'
# Should print the shared key — both sides need API_PROXY_KEY
docker compose --env-file .env.openclaw exec openclaw-gateway env | grep API_PROXY_KEYThe first heartbeat fires ~30 min after the gateway starts. It pulls 7 days of history as a backfill.
All commands use --env-file .env.openclaw.
# Up / down
docker compose --env-file .env.openclaw up -d
docker compose --env-file .env.openclaw down
# Restart gateway (after editing openclaw.json or workspace prompts)
docker compose --env-file .env.openclaw restart openclaw-gateway
# Recreate gateway (after editing .env.openclaw — restart won't reload env vars)
docker compose --env-file .env.openclaw up -d openclaw-gateway
# Recreate api-proxy (after editing project-config.json or .env.api)
docker compose --env-file .env.openclaw up -d --force-recreate api-proxy
# Rebuild api-proxy (after changing code in api/)
docker compose --env-file .env.openclaw build api-proxy
docker compose --env-file .env.openclaw up -d api-proxy
# Rebuild gateway image (after editing gateway/Dockerfile or gateway/agent-rules.md)
docker compose --env-file .env.openclaw build openclaw-gateway
docker compose --env-file .env.openclaw up -d openclaw-gateway
# Logs
docker compose --env-file .env.openclaw logs -f openclaw-gateway
docker compose --env-file .env.openclaw logs --tail=100 api-proxy
# Wiki health
docker compose --env-file .env.openclaw exec openclaw-gateway openclaw wiki status
docker compose --env-file .env.openclaw exec openclaw-gateway openclaw plugins list | grep memory-wiki.
├── docker-compose.yml
├── acpx-config.json ← ACP agent config (claude as local subprocess)
├── .env.openclaw ← gateway + agent secrets (gitignored)
├── .env.api ← api-proxy secrets (gitignored)
├── .env.openclaw.example
├── .env.api.example
├── project-config.json ← project config: list/channel ids (gitignored)
├── project-config.example.json ← template
├── google-key.json ← Google service account key (gitignored)
├── gateway/ ← merged gateway image
│ ├── Dockerfile ← FROM openclaw + claude-code + tooling
│ ├── agent-rules.md ← global CLAUDE.md for claude-code sessions
│ └── .dockerignore
├── repos/ ← agent's worktree workspace, mounted at /workspace/repos (gitignored)
├── openclaw/ ← mounted as /home/node/.openclaw
│ ├── openclaw.json ← gateway config (hardcoded channel ID for allowlist)
│ └── workspace/ ← agent prompts and runtime state
│ ├── AGENTS.md ← operating manual
│ ├── SOUL.md ← voice and doctrine
│ ├── USER.md ← people, IDs, cadence, workspace slug
│ ├── MEMORY.md ← curated long-term project memory
│ ├── IDENTITY.md ← agent identity (@alma)
│ ├── TOOLS.md ← tool reference
│ ├── HEARTBEAT.md ← per-cycle playbook
│ ├── memory/ ← cycle state (runtime)
│ └── wiki/ ← memory-wiki vault
├── api/
│ ├── Dockerfile
│ ├── main.py ← FastAPI app
│ ├── config.py ← loads project-config.json + env vars
│ ├── deps.py ← CONFIG singleton + RequireAuth
│ ├── routers/
│ │ ├── clickup.py ← /clickup/recent, /clickup/tasks, /clickup/tasks/{id}
│ │ ├── slack.py ← /slack/recent, /slack/thread, /slack/message
│ │ └── gdrive.py ← /gdrive/recent, /gdrive/docs/{id}
│ └── services/
│ ├── clickup.py
│ ├── slack.py
│ ├── gdrive.py
│ └── timestamps.py
├── CLAUDE.md
└── README.md
| File | Read by | Contains |
|---|---|---|
.env.openclaw |
Compose (--env-file) + openclaw-gateway (env_file:) |
Gateway port/token, Anthropic key, Slack bot/app tokens, SLACK_CHANNEL_ID, API_PROXY_KEY (client copy), GH_TOKEN / GITHUB_TOKEN for the agent |
.env.api |
api-proxy (env_file:) only |
API_PROXY_KEY (server copy), CLICKUP_TOKEN, SLACK_TOKEN, GOOGLE_KEY_PATH |
API_PROXY_KEY lives in both files with the same value — it's the shared interface secret. Everything else stays on one side only.
- Slack app setup — how to create and configure the Slack app for OpenClaw
- GCP hosting guide — creating a VM, installing Docker, and setting up an SSH tunnel
- Memory-wiki plugin — documentation for the memory-wiki plugin
- Secrets live only in
.env.openclawand.env.api, both gitignored. - Project config (
project-config.json) is also gitignored — only the sanitizedproject-config.example.jsonis committed. - The gateway binds to
127.0.0.1— remote access via SSH tunnel only. - The api-proxy has no host binding — only containers on
openclaw-netcan reach it. - The agent never messages clients directly. All proposals require human confirmation in Slack.