Codeq8 logoCodeq8
DocsSelf-hosting

Self-hosting setup guide

Use this guide when you want Codeq8 chats to run on your own GitHub Actions self-hosted runner. You sign in, connect GitHub App access, select a repository, register a runner, and commit one workflow file.

Runtime secret mapping is optional and lives in the separate secrets guide.

Before you start

Use a GitHub account that can access the target repository and a prepared self-hosted runner machine.

What Codeq8 needs

GitHub App access, one workflow file, and an online self-hosted runner.

What you get

Chat-driven repository work that produces normal branches, commits, and pull requests.

1

Sign in

Start at the homepage and authenticate with GitHub. Codeq8 uses that session to show repositories your account can reach.

  1. Open Codeq8 and choose Sign in.
  2. Complete the GitHub OAuth prompt.
  3. Return to Codeq8 after GitHub redirects you back to the app.
2

Select a repository in Codeq8

After sign-in, Codeq8 shows repositories discovered from your GitHub login and the Codeq8 GitHub App installation.

  1. Open the repository picker in the left sidebar.
  2. Select the repository from the available list.
  3. If the repository is missing, manage the GitHub App installation in GitHub and grant that repository access.

To add another organization later, use the GitHub App access action in Codeq8 and grant access in GitHub.

3

Manage GitHub App access

Codeq8 needs GitHub App access so it can read repository metadata, dispatch chat runs, and create branches or pull requests when a chat asks for code changes.

  1. Open the Codeq8 GitHub App installation page when the app prompts you.
  2. Install it on the organization or account that owns the repository.
  3. Grant access to the repository you want Codeq8 to use, or choose all repositories if that is how your organization manages GitHub Apps.
  4. Return to Codeq8 and select the repository from the refreshed list.
4

Register a self-hosted runner

Codeq8 chat runs are delegated to GitHub Actions, so the repository needs access to an online self-hosted runner.

Choose repository or organization scope

Repository runner

Use this when one repository should own the runner. It is the smallest access surface and is usually the easiest first setup.

Organization runner

Use this when multiple repositories in the same organization should share runner capacity. Limit the runner or runner group to the repositories that should be allowed to use it.

The workflow below uses runs-on with the default self-hosted label, so either a repository runner attached to this repository or an organization runner allowed for this repository can pick up Codeq8 jobs. GitHub recommends using self-hosted runners with private repositories unless you have reviewed the fork pull-request security implications.

GitHub keeps the full runner UI reference in its self-hosted runner setup docs.

Prepare the runner machine

Install the Codex CLI as codex on the runner or set CODEX_PATH to the executable, then sign in to Codex on that same runner account before sending a Codeq8 chat.

Install GitHub CLI as gh too. Codeq8 supplies scoped GitHub tokens during a run, but Codex needs the local gh binary available for GitHub CLI operations.

Add a repository runner

  1. Open the repository on GitHub.
  2. Go to Settings, then Actions, then Runners.
  3. Choose New self-hosted runner.
  4. Select the runner machine operating system and architecture.
  5. Run GitHub's download, configure, and start commands on the machine that should execute Codeq8.

Add an organization runner

  1. Open the organization on GitHub.
  2. Go to Settings, then Actions, then Runners.
  3. Choose New runner, then New self-hosted runner.
  4. Select the runner machine operating system and architecture.
  5. Run GitHub's download, configure, and start commands on the runner machine.
  6. Confirm the Codeq8 repository is allowed to use the runner or runner group.

Keep it online

  1. Run the runner as a service or keep the runner process open.
  2. Confirm the runner appears as Online in GitHub.
  3. Confirm the runner has the default self-hosted label.

Verify Codeq8 can use it

  1. Send a small Codeq8 chat after the workflow is committed.
  2. Confirm GitHub Actions starts the Codeq8 chat run workflow.
  3. If the job stays queued, check that this repository can use the runner and that the runner process is still online.
5

Add the chat-run workflow

Keep the workflow filename exact. The workflow must live in the repository you added to Codeq8.

codeq8-chat-run.yml

Repository work from Codeq8 chat.

.github/workflows/codeq8-chat-run.yml

Workflow contents

Use this as the starting point for a repository that should accept Codeq8 runs. Commit it at .github/workflows/codeq8-chat-run.yml.

.github/workflows/codeq8-chat-run.yml
name: Codeq8 chat run

on:
  workflow_dispatch:
    inputs:
      run_payload_json:
        description: JSON payload for a Codeq8 web-chat run
        required: true
        type: string
  repository_dispatch:
    types:
      - codeq8_web_chat_run

concurrency:
  group: codeq8-chat-run-${{ github.repository }}-${{ github.event.client_payload.run_payload.thread_id || fromJSON(github.event.inputs.run_payload_json || '{}').thread_id || github.run_id }}
  cancel-in-progress: true

jobs:
  chat_run:
    name: Codeq8 chat run
    runs-on:
      - self-hosted
    timeout-minutes: 4320
    permissions:
      contents: write
      pull-requests: write
    steps:
      - name: Bootstrap persistent workspace checkout
        uses: Codeq8/codeq8-utils@v1
        with:
          github_token: ${{ github.token }}
          target_sha: ${{ github.sha }}
          fallback_ref: ${{ github.ref }}

      - name: Run Codeq8 chat bridge
        uses: Codeq8/codeq8-action@v1
        with:
          github_token: ${{ github.token }}

Keep the concurrency block in this workflow. It groups runs by repository and chat thread so a new message in the same Codeq8 chat lets GitHub Actions cancel the older in-progress run with native cancel-in-progress, while unrelated threads can continue.

6

Send the first chat message

After the GitHub App, runner, and workflow are ready, send a small first message so you can verify the complete path.

  1. Open the repository in Codeq8.
  2. Start a new chat and ask for a small, reviewable task.
  3. Watch GitHub Actions start the Codeq8 chat run workflow.
  4. Review the branch or pull request Codeq8 opens before merging any change.

Optional cleanup

Use a repository-owned cleanup script when Codeq8 runs may start local servers, emulators, workers, containers, or tunnels on a persistent self-hosted runner.

.github/workflows/codeq8-chat-run.yml
      - name: Clean repo runtime before Codeq8
        shell: bash
        working-directory: ${{ github.workspace }}
        run: node ./scripts/ci/codeq8-runner-cleanup.js

      - name: Run Codeq8 chat bridge
        uses: Codeq8/codeq8-action@v1
        with:
          github_token: ${{ github.token }}

      - name: Clean repo runtime after Codeq8
        if: always()
        shell: bash
        working-directory: ${{ github.workspace }}
        run: node ./scripts/ci/codeq8-runner-cleanup.js

Where it belongs

Put cleanup immediately before the Codeq8 action and repeat it after the action with if: always(). The first cleanup recovers from stale work left by an older cancelled run. The final cleanup handles the current run when GitHub Actions reaches the end of the job.

What it should do

Keep scripts/ci/codeq8-runner-cleanup.js repo-owned, idempotent, and bounded to services your repository starts. Prefer PID files, known ports, named containers, or explicit service commands. Avoid broad machine-wide process kills on shared runners.

Example cleanup script

Start with a small script like this and adapt the tracked process names to your repository. PID files should be written by the commands that start local services during a Codeq8 run.

scripts/ci/codeq8-runner-cleanup.js
#!/usr/bin/env node
const { spawnSync } = require("node:child_process");
const fs = require("node:fs");
const path = require("node:path");

const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
const runtimeDir = path.join(workspace, ".codeq8-runtime");

const trackedPidFiles = [
  path.join(runtimeDir, "dev-server.pid"),
  path.join(runtimeDir, "preview-worker.pid"),
];

function stopTrackedProcess(pidFile) {
  if (!fs.existsSync(pidFile)) {
    return;
  }

  const relativePath = path.relative(workspace, pidFile);
  const pid = Number(fs.readFileSync(pidFile, "utf8").trim());
  fs.rmSync(pidFile, { force: true });

  if (!Number.isSafeInteger(pid) || pid <= 1) {
    console.warn("Ignoring invalid pid from " + relativePath);
    return;
  }

  try {
    process.kill(pid, "SIGTERM");
    console.log("Stopped tracked process " + pid + " from " + relativePath);
  } catch (error) {
    if (error.code !== "ESRCH") {
      throw error;
    }
  }
}

function runIfAnyFileExists(command, args, candidateFiles) {
  if (!candidateFiles.some((file) => fs.existsSync(path.join(workspace, file)))) {
    return;
  }

  const result = spawnSync(command, args, {
    cwd: workspace,
    stdio: "inherit",
  });

  if (result.error?.code === "ENOENT") {
    console.warn(command + " is not installed; skipping optional cleanup");
    return;
  }

  if (result.status !== 0) {
    throw new Error(command + " cleanup failed with status " + result.status);
  }
}

for (const pidFile of trackedPidFiles) {
  stopTrackedProcess(pidFile);
}

runIfAnyFileExists(
  "docker",
  ["compose", "-p", "example-repo-codeq8", "down", "--remove-orphans"],
  ["compose.yml", "compose.yaml", "docker-compose.yml"],
);

fs.rmSync(runtimeDir, { recursive: true, force: true });