--- type: reference subtype: api-integration title: "Forgejo account visibility gates anonymous repo access — and the self-service API can't change it" summary: Why a public Forgejo repo can still 404 anonymously (owner account visibility "limited" hides all its repos), why PATCH /api/v1/user/settings silently ignores the visibility field on Forgejo 1.21, and why raw-file fetch checks should use GET not HEAD. tags: - type/reference - tool/forgejo scope: global last_updated: 2026-07-15 date: 2026-07-15 last_reviewed: 2026-07-15 related: - tea-cli-assignee-gotchas - tea-cli-comment-blocks-on-stdin source: llf-schema --- # Forgejo account visibility gates anonymous repo access Observed on Forgejo 1.21.11 (forgejo.swansoncloud.com), 2026-07-15, while publishing a public releases repo owned by an account whose visibility was `limited`. ## The gotcha A repo with `private: false` is **not necessarily anonymously reachable**. If the owning *account*'s user-level `visibility` is `limited`, ALL of that account's repos are hidden from unauthenticated visitors — anonymous requests to the repo and its `/raw/branch/...` URLs return **404**, even though the repo itself is marked public and authenticated fetches work fine. The 404 (not 403) makes it look like a wrong URL rather than an access problem. ## The API trap The self-service endpoint `PATCH /api/v1/user/settings` **returns 200 but silently ignores** a `visibility` field — the `UserSettingsOptions` schema (check the instance's own swagger) has no such property, so the server drops it without error. Account visibility can only be changed via: - the web UI: **Settings → Account → Visibility**, or - the admin endpoint `PATCH /api/v1/admin/users/` with `{"visibility": "public"}` (requires a token with `write:admin` scope — being an admin user is not enough; the token itself must carry the scope). Flipping an account `limited → public` only exposes repos already marked public; `private: true` repos stay hidden either way. ## Raw-file fetch checks: use GET, not HEAD The same instance rejects HEAD on some raw-file routes with **405**. For "is this zip URL fetchable?" verification (e.g., before `wp plugin install `), use `curl -s -o /dev/null -w '%{http_code}'` (GET) — it is also what the real consumer does.