The four pull_request_target review jobs in claude-bot.yml (Senior Developer / QA / Tester / Arbiter and their shared rubric) are replaced by a single review job running the official code-review plugin - the same skill behind Anthropic's hosted Code Review and the review workflow /install-github-app generates. The hosted service needs a Team/Enterprise organisation, so the plugin runs in CI on the maintainer's subscription instead: inline findings on PR open and ready-for-review, plus manual (re-)review when the owner or a collaborator comments "@claude review". The official example triggers on pull_request, but GitHub withholds secrets from fork runs and essentially every 3x-ui pull request is from a fork, so the job keeps the lanes' pull_request_target posture: the workspace is the base revision and nothing from the pull request is checked out or executed. What the lanes uniquely knew is distilled into REVIEW.md, handed to the skill via --append-system-prompt and pinned by bot_context_test.go the way repo-context.md is: the runtime.Runtime dispatch rule, migration and upgrade safety, the four-step route contract chain including the unchecked docs copy, the i18n rule, the three link implementations, and the wire-format verification bar. The mention job now ignores "@claude review" comments on pull requests so the review trigger does not also wake the generic bot, and the lane-only rubric file goes with the lanes. The remaining prompts also lose their tone micro-rules (no emoji, no exclamation marks, no filler) and the workflow's comment banners are removed.
9.9 KiB
Repository context for the Claude bot
Shared briefing for the jobs in .github/workflows/claude-bot.yml. It exists so
these facts live in ONE place next to the code instead of being restated in each
prompt, where they went stale silently. (Pull-request review is separate: its
code-review skill is briefed with CLAUDE.md and REVIEW.md, not this.)
CLAUDE.md, frontend/CLAUDE.md and docs/architecture.md outrank this file.
Where they disagree with it, they win and this file is the thing to fix.
docs/architecture.md carries a "Symptom -> File" index and the cron-job table,
which answer "which file owns X" in one hop; grepping blind wastes turns on a
question it already answers.
Stack
3x-ui is an open-source web control panel for managing Xray-core servers.
- Backend: Go 1.26, module
github.com/mhsanaei/3x-ui/v3, Gin and GORM. - It runs Xray-core as a managed child process (
internal/xray/process.go) and importsgithub.com/xtls/xray-corefor config types and the gRPC stats/handler/router API. The release the panel BUNDLES is pinned inDockerInit.sh; the version it COMPILES against is pinned ingo.mod, and the two are not always the same. - MTProto inbounds run a SECOND managed child, the
mtg-multibinary (a multi-secret mtg fork, panel-side code ininternal/mtproto/), one process per inbound. Client, ad-tag and quota/expiry edits are hot-applied through the fork's management API (PUT /secrets) so connections survive, with a process restart as the fallback on older binaries. - Storage: SQLite by default (
/etc/x-ui/x-ui.dbon Linux, the executable directory on Windows) or PostgreSQL (XUI_DB_TYPE/XUI_DB_DSN). The SQLite driver is CGo, soCGO_ENABLED=0builds fail. - Frontend: React 19 + Ant Design 6 + Vite 8 + TypeScript in
frontend/, built intointernal/web/dist/(gitignored) and embedded withembed.FS.
Where things live
| area | path |
|---|---|
entry point + x-ui CLI |
main.go |
| env parsing | internal/config/ |
| schema, migrations | internal/database/, internal/database/model/ |
| Xray child process + config | internal/xray/ |
| MTProto inbounds | internal/mtproto/ |
| subscription server | internal/sub/ |
| HTTP handlers | internal/web/controller/ |
| business logic | internal/web/service/ |
cron jobs (schedules in web.go startTask()) |
internal/web/job/ |
| master/sub-node over mTLS | internal/web/runtime/ |
| i18n | internal/web/locale/, internal/web/translation/ |
| UI source | frontend/src/ |
| install / upgrade | install.sh, x-ui.sh, DockerInit.sh |
Hard rules a change must respect
- Dispatch through
runtime.Runtime. Every state-changing inbound or client operation goes through the interface ininternal/web/runtime/, never straight tointernal/xray/api.go. A direct call passes every local test and silently breaks every multi-node deployment; it is invisible in a single-box reading of a diff. - Layering. Controllers are thin — bind, validate, respond — with no GORM
queries, no Xray calls and no business rules.
internal/util/*is leaf-only and must not import service, controller or database.internal/web/dist/andfrontend/src/generated/are generated; a hand-edit is a violation. - Comments in committed Go/TS/TSX: 2 lines MAX per block, spent on the why
a name cannot hold — an invariant, an issue number, a non-obvious constraint.
Exempt, never flag:
//go:build,//go:generate,//nolint:,// Code generated ... DO NOT EDIT.. HTML<!-- -->is fine. - The route contract chain, which breaks in four distinct places:
- a new
g.POST/g.GETininternal/web/controller/needs a matching entry infrontend/src/pages/api-docs/endpoints.ts— pinned BOTH ways byTestRouteRegistryContractininternal/web/routes_contract_test.go, so a renamed or removed route that leaves a stale entry fails too; - generated artefacts must be regenerated with
make gen, or CI'scodegenjob fails on a dirtyfrontend/src/generatedorfrontend/public/openapi.json; - a NEW struct crossing the API boundary must be added to the
StructAllowallowlist intools/openapigen/main.go, or it is SILENTLY dropped from the schemas andfrontend/scripts/build-openapi.mjsthen fails — a guaranteed CI break, not a style nit; - the step NOTHING checks —
frontend/public/openapi.jsonmust be copied todocs/public/openapi.jsonand the MDX regenerated withcd docs && pnpm gen:api, becausedocs-ci.ymlfires only ondocs/**. Step 4 is the one that reaches production wrong.
- a new
- i18n. A new English key goes in EVERY locale JSON in
internal/web/translation/(13 files) AND must be referenced fromfrontend/srcor Go in the SAME change.frontend/src/test/i18n-dead-keys.test.tsfails on a missing locale file and on an orphan key alike. - Migrations. Schema changes are GORM
AutoMigratePLUS hand-written migrations ininternal/database/db.go. There are no migration files and no down-migrations, and everything has to work on SQLite AND PostgreSQL. - Tests. Stdlib
testingonly (no testify), table-driven witht.Runsubtests andt.Helper()on helpers. An assertion must pin the exact value, typed error or emitted string —err != nilandlen(x) > 0are findings, not nits. Prefer real dependencies: a throwaway DB viadatabase.InitDB(filepath.Join(t.TempDir(), "x-ui.db"))witht.Cleanup, andhttptestfor HTTP.internal/sub'sinitSubDB(t)is the template. A test must FAIL without its fix; one that passes either way certifies nothing and then gets cited as proof the fix works.
The three link implementations
Link and subscription generation is implemented three times, independently:
| language | path | what it feeds |
|---|---|---|
| Go | internal/util/link/, internal/sub/ |
what the panel serves |
| TS | frontend/src/lib/xray/ |
what the panel UI shows |
| TS | docs/lib/xray/ |
what the docs site shows |
A change to share-link or subscription output that touches one and not the others is how they drift apart.
Downstream programs that must accept what the panel emits
- XTLS/Xray-core — the Xray config the panel generates, and the VLESS/VMess transport and security fields.
- MetaCubeX/mihomo — consumes the Clash YAML from
internal/sub/. - SagerNet/sing-box — parses the share links the panel emits.
- mhsanaei/mtg-multi — the MTProto sidecar whose TOML (
[secrets],[secret-ad-tags],[secret-limits]) and management API (PUT /secrets,POST /secrets/{name}/reset-quota)internal/mtproto/writes and calls.
What CI runs
.github/workflows/ci.yml, on every pull request touching Go or frontend code.
It is paths-filtered, so a docs-only or workflow-only change produces no run.
| job | what it proves |
|---|---|
go-test |
go test -shuffle=on -count=1 over every package except frontend/node_modules |
race |
the same set under -race -shuffle=on |
postgres-durable-first |
live PostgreSQL 16: the PostgresCommitFailure tests plus TestHostAutoMigrateCreatesColumns_Postgres and TestMigrate_Postgres. Both steps COUNT passes rather than assert on SKIP, so a renamed or deleted test fails the job |
govulncheck |
known vulnerabilities |
golangci |
golangci-lint |
fuzz-smoke |
30s each on FuzzParseLink and FuzzDecodeCertPin |
codegen |
npm run gen then git diff --exit-code on the generated files |
frontend |
MSW worker drift, lint, format:check, typecheck, npm test (Vitest + headless-Chromium Storybook), build, build-storybook, npm audit |
What CI does NOT prove. These test families t.Skip unless an environment
variable is set, and CI sets only the PostgreSQL ones above:
| gate | covers |
|---|---|
XUI_TEST_PG_DSN |
PostgreSQL-specific paths |
XUI_DB_TYPE + XUI_DB_DSN |
dialect-dependent behaviour |
XRAY_E2E_BINARY |
the Xray gRPC end-to-end tests in internal/xray/ |
XUI_SCALE_TEST |
scale tests in internal/sub/, internal/web/job/, internal/web/service/ |
Mutation testing (mutation.yml) runs nightly and never on a pull request, so a
test that cannot fail is invisible to CI. make verify is the local gate.
Support facts reporters get wrong
- Linux install:
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) - Install generates a RANDOM username, password and web base path — never
admin/admin. The
x-uimenu on the server shows or resets them. - The installer service environment file is DISTRO-DEPENDENT:
/etc/default/x-ui(Debian/Ubuntu),/etc/conf.d/x-ui(Arch),/etc/sysconfig/x-ui(RHEL/Fedora). Naming the wrong one means the reporter's edit is silently never read by systemd — a common cause of "I set the variable and nothing happened". - Windows is supported. There the database sits next to the executable, not in
/etc— never quote the Linux path to a Windows user. - SQLite to PostgreSQL:
x-ui migrate-db --dsn "postgres://...", then setXUI_DB_TYPE/XUI_DB_DSNin that file andsystemctl restart x-ui. The source SQLite file is left in place. - Docker image
ghcr.io/mhsanaei/3x-ui; PostgreSQL profiledocker compose --profile postgres up -d. Fail2ban IP-limit enforcement needsNET_ADMIN+NET_RAW(compose grants them; a baredocker runmust add--cap-add=NET_ADMIN --cap-add=NET_RAW). - Never state that a
XUI_*variable does not exist without greppinginternal/config/andinternal/tunnelmonitor/first. TheXUI_TUNNEL_HEALTH_*family is the usual answer to "the panel restarts Xray every few minutes". - Security per inbound is none / tls / reality. XTLS is a VLESS flow
(
xtls-rprx-vision), not a security setting — never tell anyone to pick XTLS in the security dropdown. - Never hardcode a version. For "is this already fixed" use
gh release list -L 10,gh search commits, andgit log -S.