# Installing ChannelGate on a new machine 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](./README.md). ## 1. Prerequisites - **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 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. ```bash 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): ```bash 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`](./docs/OPERATIONS.md#container-runtime). - A **Slack workspace** where you can create an app. ## 2. Get the code ```bash git clone https://github.com/makeitfutureDev/channelgate.git cd channelgate ``` ## 3. Run the installer ```bash 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: ```bash 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: ```bash npm run whisper:install ``` #### 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: ```bash 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`: ```bash 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= 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`](./docs/OPERATIONS.md#service-identities). The admin UI + health endpoint come up on (set `PORT` in `.env` to change). ### Safe updates 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) 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 → **Create New App** → **From a manifest**. 2. Pick your workspace, paste the contents of [`slack-app-manifest.json`](./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`. ## 5. Configure tokens Open → **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`.) ### License key 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`](docs/LICENSE-KEYS.md); a key is never required for the daemon to run, and an unlicensed or unreachable state never kills a run. ## 6. Set up access 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. ## 7. Use it - **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**. ### Set up a first team workflow 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](./docs/OPERATIONS.md#container-runtime) 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](./docs/SKILLS.md). 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](./docs/OPERATIONS.md#reverse-proxies-and-tunnels). ## Backup & restore config (encrypted) 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: ```bash 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:** ```bash 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. ## Where data lives Channel **working folders** (the "discussions" — where the agent runs, reads/writes files, uploads, `AGENTS.md`) live in a visible **`~/ChannelGate///`** (override the root with `CG_WORKSPACE_DIR`), where `` 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///`. ### Upgrading from Claude Gateway for Slack 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//` → `~/ChannelGate///`, 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: ```bash 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: ```bash 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: ```bash 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`. ## Troubleshooting - **"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`.