CodeNavi in your PR review bot (GitHub Actions / Forgejo Actions)¶
A CI review bot is a coding agent run headless on a runner: a workflow fires on a pull
request, the agent reads the diff, and posts a review. Any bot that can load an MCP
server can use CodeNavi as its eyes on the codebase — and the one tool a reviewer
actually needs, diff_impact, is built for exactly this job.
The bot reads your diff; your codebase never leaves the box. The index, the embedder, the call graph and the search all stay on the CodeNavi host. What leaves is the diff the runner already has and the answers the bot asks for — not the repository. CI review is usually the moment a team first pipes whole repos to a cloud model. With CodeNavi in the loop, the model asks questions about the code instead of reading it.
This page shows the recipe for Anthropic's
claude-code-action on GitHub and its
Forgejo-capable fork on a self-hosted
forge. The same --mcp-config / --allowedTools shape works for any headless agent that
takes Claude Code CLI arguments.
What the bot gets¶
| Tool | Why a reviewer wants it |
|---|---|
diff_impact |
Paste the PR's unified diff → every caller of every symbol the diff touches, test files flagged separately. Blast radius before merge; no competitor's bot has this. |
impact_analysis |
Blast radius of one named symbol (when the diff touches a hub). |
find_callers |
Who calls this function — reverse call-graph lookup. |
semantic_search |
"Is there already a helper for this?" — concept search, not grep. |
read_file |
Read the flagged files (lock-free, safe to batch). |
Keep the allowlist to these five plus a read-only git diff. A review bot has no business
reindexing or writing notes; the defaults below give it none of that.
Prerequisites¶
- A running CodeNavi with the PR's repository indexed (
list_codebasesshows its name — a single-codebase install is usuallydefault). CODENAVI_AUTH_TOKENset in the server's.env— never run an unauthenticated endpoint that a runner can reach.- The runner can reach the CodeNavi host: LAN, tailnet, or a tunnel — see remote-access.md. GitHub-hosted runners cannot see your LAN; use a self-hosted runner or a tunnel. Forgejo runners are self-hosted by definition.
- Two repository (or org) secrets:
CODENAVI_URL— e.g.http://codenavi.internal:8765/mcp/myrepo(scoped: every search is pinned to that codebase, whatever the prompt asks for)CODENAVI_AUTH_TOKEN— the bearer token from the server's.envANTHROPIC_API_KEY(or the Bedrock / Vertex / Foundry equivalent the action supports).
The MCP config¶
Both workflows below pass the same JSON to Claude Code's --mcp-config:
{
"mcpServers": {
"codenavi": {
"type": "http",
"url": "${{ secrets.CODENAVI_URL }}",
"headers": { "Authorization": "Bearer ${{ secrets.CODENAVI_AUTH_TOKEN }}" }
}
}
}
The server name codenavi is what the tool names hang off: Claude Code exposes MCP tools
as mcp__<server>__<tool>, so the allowlist names mcp__codenavi__diff_impact and so on.
Rename the server and the allowlist must follow.
GitHub Actions¶
.github/workflows/codenavi-review.yml:
name: CodeNavi PR review
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
review:
runs-on: self-hosted # must reach the CodeNavi host; see Prerequisites
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history so `git diff` against the base works
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Review this pull request using CodeNavi.
0. Call mcp__codenavi__reveal_tools with group="relations" first — the
server's default lean profile hides diff_impact, find_callers and
impact_analysis until revealed.
1. Run `git diff origin/${{ github.base_ref }}...HEAD` and pass the full
output to mcp__codenavi__diff_impact (depth 2).
2. For every non-test file it flags, read_file it and check whether the
change breaks the caller. Use find_callers / impact_analysis on any
symbol marked ⚠ Hub.
3. Use semantic_search once to check whether the diff re-implements
something that already exists.
4. Post one review comment: blast radius (files + callers), concrete
breakages with file:line, and a merge/hold recommendation.
Keep one or two CodeNavi searches in flight, never a burst: they serialize on the GPU.
claude_args: |
--mcp-config '{"mcpServers":{"codenavi":{"type":"http","url":"${{ secrets.CODENAVI_URL }}","headers":{"Authorization":"Bearer ${{ secrets.CODENAVI_AUTH_TOKEN }}"}}}}'
--allowedTools "Bash(git diff:*),mcp__codenavi__reveal_tools,mcp__codenavi__diff_impact,mcp__codenavi__impact_analysis,mcp__codenavi__find_callers,mcp__codenavi__semantic_search,mcp__codenavi__read_file"
--max-turns 25
Forgejo Actions (self-hosted forge)¶
Anthropic's action is GitHub-only. The vtmocanu/claude-code-action fork adds native Forgejo support (REST-only, dedicated bot user, platform auto-detection). Two Forgejo-specific facts shape the workflow:
- Forgejo runners pre-fetch every
uses:reference before evaluatingif:, and resolve shortowner/repo@v1refs against the instance's default actions source (code.forgejo.orgby default, which mirrorsactions/checkoutbut not this fork). Mirror the action into your forge and reference it by full URL. - There is no
claude[bot]: create a bot user (claude), give it arepo+user:readtoken, and pass its identity explicitly. The fork'sdocs/forgejo.mdwalks through it.
.forgejo/workflows/codenavi-review.yml:
name: CodeNavi PR review
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
review:
runs-on: ubuntu-latest # a label your registered runner carries
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: https://forge.example.com/actions/claude-code-action@v1 # your mirror
with:
platform: "forgejo" # always explicit; the URL heuristic misfires on GHES
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.CLAUDE_BOT_TOKEN }}
bot_id: "42" # curl -s https://forge.example.com/api/v1/users/claude | jq .id
bot_name: "claude"
bot_email: "[email protected]"
prompt: |
Review this pull request using CodeNavi.
0. Call mcp__codenavi__reveal_tools with group="relations" first — the
server's default lean profile hides diff_impact, find_callers and
impact_analysis until revealed.
1. Run `git diff origin/${{ github.base_ref }}...HEAD` and pass the full
output to mcp__codenavi__diff_impact (depth 2).
2. For every non-test file it flags, read_file it and check whether the
change breaks the caller. Use find_callers / impact_analysis on any
symbol marked ⚠ Hub.
3. Use semantic_search once to check whether the diff re-implements
something that already exists.
4. Post one review comment: blast radius (files + callers), concrete
breakages with file:line, and a merge/hold recommendation.
Keep one or two CodeNavi searches in flight, never a burst: they serialize on the GPU.
claude_args: |
--mcp-config '{"mcpServers":{"codenavi":{"type":"http","url":"${{ secrets.CODENAVI_URL }}","headers":{"Authorization":"Bearer ${{ secrets.CODENAVI_AUTH_TOKEN }}"}}}}'
--allowedTools "Bash(git diff:*),mcp__codenavi__reveal_tools,mcp__codenavi__diff_impact,mcp__codenavi__impact_analysis,mcp__codenavi__find_callers,mcp__codenavi__semantic_search,mcp__codenavi__read_file"
--max-turns 25
The fork's docs/forgejo.md also lists what Forgejo does not have (no commit signing,
no GraphQL, different webhook event names) — none of it affects the CodeNavi part.
The three things that bite people¶
The default tool profile hides the relations tools. Since 1.3.23 the server advertises an
11-tool core and loads the rest on demand: diff_impact, find_callers and impact_analysis
stay hidden until reveal_tools(group="relations") runs, which is why both recipes allow-list
it and call it first. If the bot cannot use a freshly revealed tool within the same run, start
the server the bot talks to with CODENAVI_TOOL_PROFILE=full instead — the profile is per
server, not per endpoint.
diff_impactmatches the diff's line numbers against the index. The index must cover the PR's base branch and be fresh. Reindex the codebase (incremental — seconds) on merges to the base branch; a webhook or a cron on the CodeNavi host does it (see webhooks.md). A stale index turns a blast radius into a guess. The result is a lower bound either way — the call graph is name-based and static.- The runner must reach the CodeNavi host, and only the CodeNavi host must be
reachable. Scope
CODENAVI_URLto/mcp/<codebase>so every search is pinned to the PR's codebase — note thatlist_codebasesstill returns every index's name on a scoped endpoint; scoping pins queries, it does not hide the catalog. KeepCODENAVI_AUTH_TOKENa secret, not a variable, and do not put the reindex or note tools on the allowlist. - One or two searches in flight. Searches serialize on their GPU phases; a
burst queues and, past 15 s, returns busy. The prompt says so, and
--max-turnskeeps a runaway review bounded. Expect a few cents per PR review at these limits.
Verify before wiring the workflow¶
Run the same thing from a shell on the runner (or any box that can reach the host) — if this works, the workflow will:
export CODENAVI_URL=http://codenavi.internal:8765/mcp/myrepo
export CODENAVI_AUTH_TOKEN=...
claude -p "call list_codebases, then run diff_impact on this diff: $(git diff HEAD~1)" \
--mcp-config "{\"mcpServers\":{\"codenavi\":{\"type\":\"http\",\"url\":\"$CODENAVI_URL\",\"headers\":{\"Authorization\":\"Bearer $CODENAVI_AUTH_TOKEN\"}}}}" \
--allowedTools "mcp__codenavi__list_codebases,mcp__codenavi__diff_impact"
Verified run (laravel testbed, one-line hunk inside Str::snake): diff_impact at depth 2
returned 156 affected files — 79 code, 77 test — with the Eloquent relation/foreign-key
hubs flagged, and the review correctly noted the hunk was comment-only so the realistic
test set was SupportStrTest + SupportStringableTest. Wide radius on a trivial hunk is
expected: it is the static lower-bound the reviewer then narrows.
A 401 means the token does not match the server's .env; an Unknown codebase error
means the /mcp/<name> scope is wrong — list_codebases on the unscoped /mcp shows the
real names. Full status codes: api-reference.md.
Other headless agents¶
The recipe is not Claude-specific. Any agent that runs headless and loads MCP servers from
a config file works the same way — the only thing that changes is the config shape
(Codex: ~/.codex/config.toml with bearer_token_env_var; Antigravity: serverUrl +
literal token; see connecting-codex.md and
connecting-antigravity.md). Give the bot the same five tools
and the same prompt.