Your AI coding assistant doesn’t know about your GitHub issues. When you ask “Are there any open bugs related to auth?” it can only guess. With a short Lua connector, you can index every issue (with comments) and make them part of your AI’s context.

What you get

After setup, you can:

The connector

Create connectors/github-issues.lua:

connector = {}
connector.name = "github-issues"
connector.description = "Index GitHub issues with comments from a repository"

function connector.scan(config)
    local owner = config.owner or error("config.owner is required")
    local repo = config.repo or error("config.repo is required")
    local token = config.token or env.get("GITHUB_TOKEN") or ""
    local state = config.state or "all"
    local max_pages = tonumber(config.max_pages) or 10

    local headers = {
        ["Accept"] = "application/vnd.github+json",
        ["User-Agent"] = "context-harness",
    }
    if token ~= "" then
        headers["Authorization"] = "Bearer " .. token
    end

    local items = {}
    local base = "https://api.github.com/repos/" .. owner .. "/" .. repo

    for page = 1, max_pages do
        local url = string.format(
            "%s/issues?state=%s&per_page=100&page=%d&sort=updated&direction=desc",
            base, state, page
        )

        local resp = http.get(url, { headers = headers })
        if resp.status ~= 200 then
            log.error("GitHub API error: " .. resp.status .. "" .. resp.body)
            break
        end

        local issues = json.decode(resp.body)
        if #issues == 0 then break end

        for _, issue in ipairs(issues) do
            -- Skip pull requests (they show up in the issues endpoint)
            if not issue.pull_request then
                local labels = {}
                for _, l in ipairs(issue.labels or {}) do
                    table.insert(labels, l.name)
                end

                -- Build body with issue description
                local body = issue.body or "(no description)"

                -- Fetch comments if any
                if issue.comments and issue.comments > 0 then
                    local c_resp = http.get(
                        issue.comments_url,
                        { headers = headers }
                    )
                    if c_resp.status == 200 then
                        local comments = json.decode(c_resp.body)
                        for _, c in ipairs(comments) do
                            body = body .. string.format(
                                "\n\n---\n**%s** (%s):\n%s",
                                c.user and c.user.login or "unknown",
                                c.created_at or "",
                                c.body or ""
                            )
                        end
                    end
                end

                table.insert(items, {
                    id = owner .. "/" .. repo .. "#" .. issue.number,
                    title = string.format("#%d: %s", issue.number, issue.title),
                    body = body,
                    url = issue.html_url,
                    metadata = {
                        state = issue.state,
                        author = issue.user and issue.user.login or "unknown",
                        labels = table.concat(labels, ", "),
                        created = issue.created_at,
                        updated = issue.updated_at,
                    },
                })
            end
        end

        log.info(string.format("Page %d: fetched %d issues", page, #issues))
    end

    log.info(string.format("Total: %d issues from %s/%s", #items, owner, repo))
    return items
end

return connector

Configure it

Add to ctx.toml:

[connectors.script.github-issues]
path = "connectors/github-issues.lua"
owner = "your-org"
repo = "your-repo"
token = "${GITHUB_TOKEN}"
state = "all"          # "open", "closed", or "all"
max_pages = 20         # 100 issues per page, 20 pages = 2000 issues

For private repos, create a GitHub personal access token with repo scope and set it as an environment variable:

export GITHUB_TOKEN="ghp_..."
$ ctx sync script:github-issues
sync script:github-issues
  Page 1: fetched 100 issues
  Page 2: fetched 100 issues
  Page 3: fetched 78 issues
  Total: 264 issues from your-org/your-repo
  fetched: 264 items
  upserted documents: 264
  chunks written: 1,203
ok

$ ctx search "authentication timeout" --source script:github-issues
1. [0.89] script:github-issues / #342: Auth tokens expire during long-running jobs
   "Users report 401 errors after ~30 minutes. The refresh token..."
2. [0.76] script:github-issues / #298: SSO session timeout too aggressive
   "When using SAML SSO, sessions expire after 15 minutes..."

Multiple repos

Index issues from your whole org:

[connectors.script.platform-issues]
path = "connectors/github-issues.lua"
owner = "acme"
repo = "platform"
token = "${GITHUB_TOKEN}"

[connectors.script.infra-issues]
path = "connectors/github-issues.lua"
owner = "acme"
repo = "infrastructure"
token = "${GITHUB_TOKEN}"
state = "open"
max_pages = 5
$ ctx sync all
# Syncs both repos in parallel

Now you can search across all repos:

$ ctx search "database migration"
# Returns issues from both platform and infrastructure

Or filter to one:

$ ctx search "database migration" --source script:platform-issues

Keep it fresh

Set up a cron job to re-sync periodically:

# Every 2 hours
0 */2 * * * cd ~/ctx-workspace && ctx sync script:github-issues

Or trigger a sync from a GitHub webhook after new issues are created.

Rate limits

The GitHub API allows 5,000 requests/hour with a token (60/hour without). Each page of issues costs 1 request, plus 1 request per issue with comments. For a repo with 500 issues where 200 have comments, that’s about 205 requests — well within limits.

Add sleep(0.1) after each http.get call if you’re indexing very large repos and want to be extra cautious.