Skip to content

Install ChannelGate

A step-by-step guide to get the gateway running on a fresh Linux host. ChannelGate runs on Linux only: the daemon is a systemd service and every channel runs inside its own rootless Podman container (Ubuntu 24.04 is the tested distribution). For what it does and how it works, see README.md.

  • Linux with systemd (Ubuntu 24.04 tested). The daemon refuses to start anywhere else.
  • Node.js ≥ 22.13 — node -v (the gateway’s SQLite store uses the built-in node:sqlite, stable from 22.13). Install from https://nodejs.org or your distribution’s packages.
  • Claude Code CLI, installed and signed in on the host as the daemon user. Turns run inside the channel containers (the runtime image ships the engine CLIs), and the gateway relays that host login’s short-lived access token into each container — the credentials file itself is never copied or mounted. Alternatives: paste a claude setup-token value in Settings → Container runtime, or set ANTHROPIC_API_KEY for a service install with no interactive login.
    Terminal window
    npm install -g @anthropic-ai/claude-code
    claude login # or set ANTHROPIC_API_KEY
    claude --version # must work
  • (Optional) OpenAI Codex CLI — only if you’ll use the Codex engine. Its host sign-in is relayed into the channel containers through the egress proxy — each container gets an access-only stand-in, never the host’s login file (sessions stay per channel):
    Terminal window
    npm install -g @openai/codex
    codex login # or set OPENAI_API_KEY
  • Rootless Podman — required: every channel runs its engines in a container of its own, and the daemon refuses to boot without a container CLI. sudo apt install podman uidmap and confirm the daemon user has /etc/subuid + /etc/subgid ranges; the installer then builds the channel image itself (npm run build:image is the manual equivalent). Setup, settings and caveats are in docs/OPERATIONS.md.
  • A Slack workspace where you can create an app.
Terminal window
git clone https://github.com/makeitfutureDev/channelgate.git
cd channelgate
Terminal window
npm run setup

This checks the Linux/Node/CLI/Podman prerequisites, installs dependencies, asks whether to provision pinned whisper.cpp plus the multilingual large-v3-turbo model, builds the channel container image, and creates .env. Choosing Whisper downloads about 1.5 GiB; the installer uses the available package manager for a missing ffmpeg when permitted, or explains which prerequisite is missing.

The channel image is where every conversation’s shared toolchain lives — the pinned engine CLIs, ffmpeg/ffprobe, OpenCV, faster-whisper and its pre-cached small speech model for the video workflow built into gateway-usage — so building it is part of the install, not a step to remember later. It takes several minutes on a cold cache and is run as the daemon user. Pass --skip-image (or set CG_BUILD_IMAGE=no) to defer it, and run npm run build:image before the first message; a build that fails is reported with that same remedy and never aborts the rest of the install.

The runtime and model are reused on subsequent setup/update runs. A no-Whisper install skips them on later updates too. For unattended installs, make the choice explicit:

Terminal window
npm run setup -- --with-whisper
npm run setup -- --without-whisper

CG_INSTALL_WHISPER=yes|no is the equivalent automation setting. To provision or repair the local assets independently:

Terminal window
npm run whisper:install

The systemd service (starts at boot, restarts on failure)

Section titled “The systemd service (starts at boot, restarts on failure)”

The service is a separate, root-only step because it creates a dedicated non-login account and hands it the checkout:

Terminal window
sudo bash scripts/install-systemd.sh # = npm run service:install, run as root

It provisions the final service user’s subordinate UID/GID ranges, user runtime and cgroup delegation, installs the host-side rclone binary used by Google Drive synchronization, then builds/probes the image in that user’s own rootless Podman store. A foreground --no-service setup installs rclone into ~/.local/bin instead. Downloads are checked against the release checksum; a failed optional install leaves Drive sync dormant and prints a manual remedy. It writes /etc/systemd/system/channelgate.service, with ProtectSystem=strict, ProtectHome=read-only and private /tmp. The daemon must permit Podman’s setuid namespace helpers; every channel container still uses no-new-privileges and dropped capabilities. API credentials belong in the 0600 /var/lib/channelgate/service.env before starting real turns. Manage it with systemctl:

Terminal window
sudo systemctl status channelgate
sudo systemctl restart channelgate
sudo journalctl -u channelgate -f
sudo bash scripts/uninstall-systemd.sh # = npm run service:uninstall — removes the unit, keeps the runtime root

To let developers SSH straight into channel containers (one key per person, granted per channel from chat, nothing listening on a port), run the one-time sudo CG_SSH_HOST=<host> bash scripts/install-ssh-access.sh after the image is built — see docs/SSH-ACCESS.md.

npm run setup -- --no-service only skips the reminder; npm start runs the daemon in the foreground without a service. The service identities, the engine credentials of the login-less account and the user-scope alternative (~/.config/systemd/user/channelgate.service) are in docs/OPERATIONS.md.

The admin UI + health endpoint come up on http://localhost:4747 (set PORT in .env to change).

Operators of any edition can update manually: sign in to the host as the service account, enter its checkout, and run npm run update. Keep the deployment on its configured upstream branch (main for stable installations). Enterprise additionally offers the dashboard Update button, Slack /update, and update_gateway; other editions show the commit count behind and manual update guidance, and managed update requests are refused by the server.

Managed updates require the service account’s systemd user manager and bus. They launch a separate transient service so restarting the daemon cannot kill the updater. A missing user bus is a reported launch failure; operators can still invoke the CLI manually from a host terminal. The updater requires an active channelgate.service under systemd (system or user scope) so it can prove both candidate and rollback restarts.

Before changing Git it checks upstream access, a clean fast-forward-only checkout, Node/npm, settings, service state, free space, current gateway health, and an isolated Claude turn. Base staging requires 1 GiB free. If local Whisper is enabled but its 1.5 GiB model is missing, the calculated requirement becomes 3 GiB; disabling local Whisper makes the large optional download explicit and skips it.

The candidate runs npm ci --include=dev (static checks need development tools even on production hosts), the production advisory gate, the full test suite, and provisioning before restart. It is accepted only after the replacement reports the expected revision, Claude is available, Slack reconnects when previously connected, and a second isolated Claude turn succeeds. A post-change failure automatically restores the prior revision and dependencies and verifies that restored build. See ~/.channelgate/logs/update.log and ~/.channelgate/update-state.json; operator recovery snapshots are saved under ~/.channelgate/update-backups/. The dashboard keeps polling through long phases and reconnects, asks for login if the restart cleared the admin session, retains the last result across refreshes, and reports a dead updater as interrupted with completion and rollback unverified. Check the log and service before retrying an interrupted update.

4. Create the Slack app (from the manifest)

Section titled “4. Create the Slack app (from the manifest)”

For an existing app, add /menu from the updated slack-app-manifest.json to its App Manifest (or Slash Commands configuration) to enable the native command. After restarting the gateway, @agent /menu also works as a normal channel message without an app configuration update.

  1. Go to https://api.slack.com/apps → Create New App → From a manifest.
  2. Pick your workspace, paste the contents of slack-app-manifest.json, and create. It preconfigures Socket Mode, all scopes, events, the Assistant feature, and the /stop and /files commands plus the Browse channel files message shortcut.
  3. Basic Information → App-Level Tokens → generate a token with connections:write → this is SLACK_APP_TOKEN (xapp-…).
  4. Install App → install to the workspace → copy the Bot User OAuth Token → SLACK_BOT_TOKEN (xoxb-…).
  5. Basic Information → copy the Signing Secret → SLACK_SIGNING_SECRET.

Open http://localhost:4747 → Settings tab → paste the three Slack tokens → Save & connect. The status banner should turn green (● Connected as @… in …).

(Alternatively, put SLACK_BOT_TOKEN / SLACK_APP_TOKEN / SLACK_SIGNING_SECRET in .env and restart — but the UI is easier, and UI settings override .env.)

A fresh install serves one conversation (channel, DM, or group) per UTC month, with 500 AI messages in it — enough to evaluate it end to end. A free key, created with your email on the ChannelGate platform, unlocks every conversation and keeps the 500-message monthly limit per conversation; an Enterprise License removes the limit.

Open Settings → License, paste the key, Save, then Verify now. (Or set CHANNELGATE_LICENSE_KEY in .env before the first boot.) The card shows the tier, when the key was last verified, and this month’s usage per conversation. Tiers, offline behaviour, and exactly what the deployment sends are in docs/LICENSE-KEYS.md; a key is never required for the daemon to run, and an unlicensed or unreachable state never kills a run.

In the admin UI:

  • Users — approve the people who may use the bot (set their Composio token if they have one). Bot users only respond to approved users; unknown users are denied even in DMs.
  • Channels — once the bot is invited to a channel and has seen a message there, it appears here. operator-approved members in a channel are auto-added to its allow-list on join. Set MCPs, skills, working folder, admin mode, or a channel-wide Composio token as needed.
  • DM the bot — it replies (no mention needed).
  • In a channel, /invite the bot, then @mention it.
  • Attach a voice clip to that mention, react 🤖 to an existing voice message, or send one in a DM. Local Whisper is tried first when enabled. Otherwise the gateway reads Slack’s completed transcript; click Generate transcript and trigger the bot again if Slack has not made one yet. Typed text is preserved as instructions and raw audio never reaches Claude or Codex.
  • Browse the channel workspace with /files; from a thread, use the Browse channel files message shortcut or mention the bot with /files.
  • Stop a run: /stop, react 🛑 on a message, or send stop in the thread — in a channel that message must @mention the bot (a DM needs no mention).
  • Switch engine (Claude ↔ Codex) any time in Settings.

For a shared workflow, start with one dedicated channel and one representative task:

  1. Invite the bot and the people who will use it. A private Slack channel limits who can see the conversation in Slack; ChannelGate still checks each sender against its own user and channel access settings. Approve users and choose the channel access policy in the admin UI.
  2. Set the channel’s working folder, tool mode and network access for that task. Keep ordinary work in the channel container. Reserve the admin-only /sudo thread for host maintenance; the runtime boundaries explain the difference.
  3. Put the source documents the agent may use in the channel workspace, and add standing instructions for durable rules. A thread resumes its own engine session; channel memory is separate and can supply useful context to later threads in that conversation. Inspect and correct saved memory as the workflow develops. Changes to channel instructions follow the gateway’s approval controls.
  4. Turn a repeated procedure into a skill with a clear trigger and steps. Grant it to the channel or its template. Review a third-party skill’s source and trial it with oversight before using it in scheduled work. Keep a workflow specific to one channel in that channel’s skill section; see the skills guide.
  5. Connect only the needed apps. Choose a requester’s personal account for personal work and a channel or organization account for shared work. Personal connections remain scoped to their owner; the selected agent connection can serve the channel. Test the chosen account with a small, representative request before relying on a larger automation.
  6. Run that task in the channel, inspect the answer and requested approvals, then adjust the instructions, skills and access settings from what actually happened. Check usage and audit history in the admin UI when reviewing the pilot.

Slack Socket Mode needs no public inbound URL. If operators need the admin UI or approval links from outside the host, put a tunnel or reverse proxy in front of the loopback service and set Settings → Connection → Public URL to its HTTPS origin. See reverse proxies and tunnels.

Your config (~/.channelgate/config + each channel’s meta.json) holds the Slack and Composio tokens, user approvals, and channel settings. Back it up encrypted, outside the repo — credential blobs never belong in git history, even encrypted:

Terminal window
npm run backup # AES-256 encrypts config → ~/.channelgate/backups/config.tar.gz.enc
  • The key is never stored with the blob:
    • default: a random key is generated once at ~/.channelgate/.backup-key (keep it safe), or
    • set your own: CG_BACKUP_PASSPHRASE='…' npm run backup (nothing is written to disk).
  • Re-run npm run backup anytime to snapshot the latest config, then copy the blob and the key somewhere safe off the machine (external drive, password manager, private storage).

Restore on another machine:

Terminal window
git clone https://github.com/makeitfutureDev/channelgate.git && cd channelgate
npm run setup
# copy your backup blob over — default location, or point CG_BACKUP_FILE at it:
# mkdir -p ~/.channelgate/backups && cp /path/to/config.tar.gz.enc ~/.channelgate/backups/
# provide the SAME key you backed up with — either:
# cp /path/to/.backup-key ~/.channelgate/.backup-key (the generated key), or
# export CG_BACKUP_PASSPHRASE='…' (your passphrase)
npm run restore # decrypts into ~/.channelgate
sudo systemctl restart channelgate # restart (or npm start)

⚠️ Without the key the backup cannot be decrypted. If you used the generated ~/.channelgate/.backup-key, copy that file somewhere safe — it’s the only way to restore.

Channel working folders (the “discussions” — where the agent runs, reads/writes files, uploads, AGENTS.md) live in a visible ~/ChannelGate/<platform>/<channel>/ (override the root with CG_WORKSPACE_DIR), where <platform> is slack, teams, or google-chat — taken from the channel’s own record, so two surfaces never share a folder. A per-channel custom working folder is used verbatim and is never namespaced. The hidden ~/.channelgate/ holds everything else (below), with per-channel metadata under channels/<platform>/<channel>/.

The first boot after the update runs scripts/migrate-channelgate.mjs automatically, before the database opens and before Slack connects. It refuses while the old daemon (or a detached background job, or a self-update transaction) is still running, then moves ~/.claude-gateway/ → ~/.channelgate/ and each channel’s ~/Slack Agent/<slug>/ → ~/ChannelGate/<platform>/<slug>/, rewrites stored absolute paths, regenerates every channel’s lockdown file, and leaves a MOVED.md breadcrumb behind. Channels with a custom working folder are left exactly where they are. Nothing is ever overwritten: an existing destination is skipped and reported.

Preview it first:

Terminal window
node scripts/migrate-channelgate.mjs --dry-run # prints the plan, changes nothing

If the migration refuses or fails, the daemon keeps serving from the OLD paths (it pins CHANNELGATE_DIR / CG_WORKSPACE_DIR back to them for that process) and logs what to clear. CLAUDE_GATEWAY_DIR and CLAUDE_GATEWAY_DB are still honoured — with a one-time deprecation warning — for one major; rename them to CHANNELGATE_DIR / CHANNELGATE_DB. Re-run sudo bash scripts/install-systemd.sh to move the service to the new unit; the installer removes the pre-rename one first.

Checkouts cloned from the previous repository. The code moved to https://github.com/makeitfutureDev/channelgate on 2026-09-03 with a fresh history (one initial commit of the scrubbed tree); the old claude-gateway-slack repository is a read-only archive. Point an existing checkout at the new repository and reset it onto the same tree — a plain git pull cannot fast-forward across unrelated histories, and the updater refuses a diverged checkout by design:

Terminal window
git remote set-url origin https://github.com/makeitfutureDev/channelgate.git
git fetch origin
git reset --hard origin/main # identical files, new history

Then restart the service once: the updater compares the running revision with the checkout’s HEAD before it touches anything. To rename the checkout directory as well (the launch layout is ~/Code/channelgate), stop the daemon, move the directory, and repath every store that recorded the old location — the channel records whose workDir is the checkout, Claude/Codex session state, the per-channel lockdown files and the installed service definition:

Terminal window
node scripts/migrate-channelgate.mjs --repath --from "$OLD_CHECKOUT" --to "$NEW_CHECKOUT"
node scripts/migrate-channelgate.mjs --verify --from "$OLD_CHECKOUT" --to "$NEW_CHECKOUT" # must print 0

The repath rewrites the service definition’s WorkingDirectory and prints the reload command (systemctl --user daemon-reload) to run before starting the service again. See docs/OPERATIONS.md → --repath.

Everything runtime is under ~/.channelgate/ (override with CHANNELGATE_DIR): config, sessions, logs, and—only when installed—the local Whisper executable in tools/whisper/v1.9.1/ plus models/whisper/ggml-large-v3-turbo.bin. This directory is not in the repo—back it up if you want to preserve config/sessions. Advanced deployments may override WHISPER_CLI_PATH, WHISPER_MODEL_PATH, WHISPER_FFMPEG_PATH, or the five-minute WHISPER_TIMEOUT_MS.

  • “localhost refused to connect” — the daemon isn’t running. systemctl status channelgate; restart with sudo systemctl restart channelgate (or npm start). Check journalctl -u channelgate and the runtime root’s logs/.
  • Bot won’t answer in a channel — the author must be approved (Users tab) and the bot must be @mentioned (DMs need neither). New channels are fail-closed.
  • Images come back as “HTML”/can’t read — the Slack app needs the files:read scope; update the manifest and reinstall the app.
  • Voice transcript is unavailable — if local Whisper is enabled, run npm run whisper:install and follow the missing ffmpeg guidance. Otherwise click Generate transcript on the Slack voice note, then mention the bot again or react 🤖. Setup treats selected local provisioning failure as fatal; an update warns and keeps the gateway online.
  • Codex 401 / won’t run — codex isn’t authenticated; run codex login.