# claude.ai on Forgejo, signed in through Forseti

Source: https://gofranz.com/blog/claude-ai-forgejo-via-forseti/

I wanted claude.ai to read my repositories on a self-hosted Forgejo, without pasting a personal access token into a connector. Turns out Forgejo 16 added the missing piece; Forseti needed one more.

### Versions

- **Forseti 0.2.24** or newer: access-token claims on resource registry rows
- **Forgejo 17.0.1** (tested); Authorized Integrations exist since 16.0
- **forgejo-mcp v3.3.0**, `resource-server` mode
- **Ory Hydra v26.2.0**

### How it fits

1. claude.ai logs in at Forseti and gets a JWT access token
2. The token carries `forgejo_aud`: the audience of your Forgejo Authorized Integration
3. forgejo-mcp checks the token, signs a 5-minute JWT for you, calls Forgejo with it
4. Forgejo matches it to your integration and applies its permissions

No forge token anywhere. forgejo-mcp's one secret is its signing key.

### Server (once)

**1. forgejo-mcp** on its own host, e.g. `mcp.git.example.com`:

```yaml
forgejo-mcp:
  image: git.b4mad.industries/agentic-forges/forgejo-mcp:v3.3.0
  command: ["--transport", "http", "--http-port", "8080"]
  environment:
    - FORGEJO_URL=https://git.example.com
    - FORGEJO_MCP_HOST=0.0.0.0
    - FORGEJO_MCP_ALLOWED_HOSTS=mcp.git.example.com
    - FORGEJO_MCP_AUTH_MODE=resource-server
    - FORGEJO_MCP_AUTHORIZATION_SERVER=https://accounts.example.com/hydra
    - FORGEJO_MCP_RESOURCE=https://mcp.git.example.com/mcp
    - FORGEJO_MCP_SCOPES_SUPPORTED=openid offline_access
    - FORGEJO_MCP_FORGEJO_JWT_ISSUER=https://mcp.git.example.com/issuer
    - FORGEJO_MCP_FORGEJO_JWT_SIGNING_KEY_FILE=/run/secrets/signing-key.pem
  volumes:
    - ./signing-key.pem:/run/secrets/signing-key.pem:ro   # EC P-256, owned by 65532
```

- `offline_access`, or the 5-minute access token means a new login every 5 minutes
- Proxy only `/mcp`, `/.well-known/oauth-protected-resource[/mcp]`, `/issuer/.well-known/openid-configuration` and `/issuer/jwks.json`; 404 the rest, never redirect
- The name has to resolve to a public IP, from Forgejo too (it refuses issuers on private addresses)

**2. Hydra** - lift the claim to the top level of the JWT (otherwise it only sits under `ext`):

```yaml
oauth2:
  allowed_top_level_claims:
    - forgejo_aud
```

**3. Forseti** - `/admin/resources/new`:

- Resource: `https://mcp.git.example.com/mcp`
- Access-token claims: `forgejo_aud`
- Only tokens for this resource get the claim; Stackpit, Sortello etc. don't

### Per user

**4. Forgejo** - Settings → Authorized Integrations → Generic JWT:

- Issuer: `https://mcp.git.example.com/issuer`
- Claim rules: `{"rules": [{"claim": "sub", "compare": "eq", "value": "<Kratos identity id>"}]}`
- Permissions: as narrow as you can live with (`read:repository`, `read:user`, ...)
- Copy the audience it shows: `u:1:<uuid>`

**5. Forseti admin** - store it on the identity (`metadata_admin` is admin-only):

```bash
curl -X PATCH http://kratos:4434/admin/identities/<id> \
  -H 'Content-Type: application/json' \
  -d '[{"op":"add","path":"/metadata_admin","value":{"forgejo_aud":"u:1:<uuid>"}}]'
```

This replaces `metadata_admin`, so carry over existing keys.

**6. claude.ai** - Settings → Connectors → Add custom connector:

- URL: `https://mcp.git.example.com/mcp`
- Leave the OAuth fields empty (claude.ai registers itself via CIMD)
- Connect, log in, approve; ask "Who am I on git.example.com?"

### Gotchas

- **The `sub` rule isn't optional.** Forgejo finds the integration by issuer and audience alone; without the rule, anyone carrying your audience acts as you.
- **The claim is fixed at consent.** Changed the audience? Disconnect and reconnect - a refresh keeps the old one.
- **The integration's permissions are the only limit.** forgejo-mcp exposes every tool to whoever logs in.
- **The signing key acts for every user** whose integration trusts the issuer. Treat it like an admin token.
- Org-scoped Forseti admins can't set claims (by design); only instance admins.
- No admin UI for `metadata_admin` yet, hence the curl. If this gets used, a field on the identity page comes next.

### Links

- [Forseti](https://github.com/franzos/forseti) ([0.2.24](https://github.com/franzos/forseti/releases/tag/v0.2.24))
- [forgejo-mcp](https://git.b4mad.industries/agentic-forges/forgejo-mcp)
- [Forgejo Authorized Integrations](https://forgejo.org/docs/latest/user/api/authorized-integrations/)
- [Forgejo 16.0 release notes](https://forgejo.org/2026-07-release-v16-0/)
