Adding an API scope
SkillSecurityGuidance for adding an API scope object to posthog/scopes.py and making it work for personal API keys, OAuth tokens and MCP clients. Use when adding a scope object, exposing a viewset or MCP tool to tokens, moving a viewset off scope_object = "INTERNAL", deciding if a scope is internal, OAuth-hidden or privileged, choosing a scope group, reading the scope list in Python, the frontend or the MCP server, or when a scope test fails in scopes.test.ts, test_scopes.py or tool-filtering.test.ts. Trigger terms: new scope, APIScopeObject, ScopeObjectEnumApi, scope_object, required_scopes, API_SCOPES, API_SCOPE_GROUPS, mcp scopes, OAuth scopes, personal API key scope.
Use Adding an API scope in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add Adding an API scope and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the Adding an API scope skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; ahel provides instructions and does not run this skill.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
What this skill tells your AI
The instructions your AI receives, as published by posthog/posthog-foss in .agents/skills/adding-api-scopes/SKILL.md and read by ahel’s review.
A scope has two parts: an object and an action, as in feature_flag:read.
Adding a scope means adding a scope object, such as feature_flag. It gives tokens feature_flag:read and feature_flag:write.
posthog/scopes.py is the source of the object list.
The frontend type and the MCP OAuth list are generated from it.
The picker rows and the groups are kept by hand in frontend/src/lib/scopes.tsx, and a test checks them.
They stay in the frontend on purpose: labels, plurals, groups and picker omissions are UI decisions, while posthog/scopes.py decides what exists and what it grants.
Decide if you need a new scope
Most endpoints fit an existing scope, for example insight:read.
Add a new scope only for a product area that a person wants to grant or withhold on its own, on a key or an OAuth app.
Name its object with a snake_case singular noun, such as feature_flag.
Decide what kind of scope it is
- Public: OAuth lists it, MCP can request it, and the key picker offers it. This is the default.
- Internal: only the server creates tokens with it. Use
INTERNAL_API_SCOPE_OBJECTS. - OAuth-hidden: a person can paste it into a personal API key, but OAuth clients do not see it. Use this for staff-only or unreleased surfaces. Use
OAUTH_HIDDEN_SCOPE_OBJECTS. - Privileged: only PostHog staff can give it to an OAuth app, through Django admin or a data migration. An app that registers itself cannot get it, and the CLI login and the key picker presets never include it. Use this for a scope that a partner app must not grant to itself, such as
llm_gateway. UsePRIVILEGED_SCOPES, and setunprivilegedExcluded: trueon the picker row.
Then decide two more things, separately from the kind:
- Project secret API keys: a project secret API key is a project credential with no user, for server-to-server calls. Allow the scope on it only when such a caller needs it. The allowed list is
PROJECT_SECRET_API_KEY_ALLOWED_API_SCOPE_ACTION, in bothposthog/scopes.pyandfrontend/src/lib/scopes.tsx. Read/adding-project-secret-api-key-authfirst. - Access control: if an organization must be able to restrict the resource per role or per object, add it to
ACCESS_CONTROL_RESOURCESinproducts/access_control/backend/facade/user_access_control.py. Access control resources use scope object names by design, so a viewset'sscope_objectnames both its token scope and its access control resource. Do not give access control a naming or a type of its own. Theresourcefields of the access control serializers takeGRANTABLE_API_SCOPE_OBJECTSas their choices, and the frontendAPIScopeObjecttype is generated from those fields (be careful: the personal API key modal, the OAuth consent screen, CLI login and the key presets also use that type).
To add the scope
- Declare it in Python. Add the object to the
APIScopeObjectliteral inposthog/scopes.py. If you chose internal, OAuth-hidden or privileged above, also add it to that set. - Connect it to its endpoints. Set
scope_object = "<object>"on each viewset the scope covers.- Standard actions need no extra work:
listandretrieveneed:read, andcreate,updateanddestroyneed:write. - A custom
@actionneedsrequired_scopes, for examplerequired_scopes=["<object>:write"]. Use:readif it only reads data, and:writeif it changes data. Without it, token requests get a 403. - An endpoint set to
scope_object = "INTERNAL"accepts only logged-in sessions. Change it to the new object to open it to tokens. - Then call each custom action with a personal API key that has only the new scope. No test checks this.
- Standard actions need no extra work:
- Regenerate. Run
hogli build:openapi, which carries the object to the frontend type, andhogli build:projections, which updates the OAuth lists of the MCP server and the web app. DO NOT EDIT GENERATED FILES BY HAND. - Show it in the pickers. Skip this step for an OAuth-hidden object: no picker shows it, and the tests do not ask for a row or a group. Otherwise, in
frontend/src/lib/scopes.tsx:- Add a row to
API_SCOPESwith a sentence-case label and plural (/writing-user-facing-copy). Disablewriteif no endpoint writes. If the key modal should not offer the object, add a reason toAPI_SCOPES_OMITTED_FROM_MODALinstead. - Add the object to one group in
API_SCOPE_GROUPS. See "Choose a group".
- Add a row to
- Let MCP tools request it. For each MCP tool that calls the endpoint, add the scope under
scopes:inproducts/<product>/mcp/tools.yaml.
Choose a group
The OAuth consent screen and the scope pickers show objects in groups, from API_SCOPE_GROUPS in frontend/src/lib/scopes.tsx.
The groups make a long list readable, so a person can find a product quickly.
- A group is a product area that a person recognizes, such as "Session replay" or "Feature flags, experiments & surveys". It is not a code module or a team.
- Put the object in the existing group that a person would look in first.
- Add a new group only when it gets two or more objects. A group with one object makes the list longer, not easier to read.
If a test fails
frontend/src/lib/scopes.test.ts, the coverage test: a grantable object has no picker row and no omission reason. Add a row toAPI_SCOPES, or a reason toAPI_SCOPES_OMITTED_FROM_MODAL. If the object is missing fromAPIScopeObject, runhogli build:openapifirst. If the object is OAuth-hidden, runhogli build:projectionsinstead, so the test learns to skip it.frontend/src/lib/scopes.test.ts, the group test: an object is in no group, or in two groups. Put it in exactly one group ofAPI_SCOPE_GROUPS. It also fails when an OAuth-hidden object has a row or a group. Remove them, because no picker shows a hidden object.posthog/test/test_scopes.py: an internal, OAuth-hidden or privileged scope leaks into a list it must stay out of, or the project secret API key list inposthog/scopes.pydiffers from the copy infrontend/src/lib/scopes.tsx. Fix the set, or make the two lists equal.services/mcp/tests/unit/tool-filtering.test.ts, the completeness test: an MCP tool requires a scope that OAuth does not advertise. If the scope is new, runhogli build:projections. If it is internal, add it to the test's server-only list. Otherwise fix the scope name intools.yaml.
What no test covers
- A custom
@actionwithoutrequired_scopes. Tokens get a 403. - A read scope on an action that changes data.
- An MCP tool scope that differs from its endpoint's
scope_object. - A group that makes no sense for the object, or a new group that should not exist. The group test only checks that each object has exactly one group. Check that it sits where a person would look for it, and that a new group has at least two objects. See "Choose a group".
- A wrong or unclear label.
Where to read scopes in code
Use these instead of writing your own list of scopes.
- Python: import from
posthog.scopes.API_SCOPE_OBJECTS: every object, internal ones too.GRANTABLE_API_SCOPE_OBJECTS: the objects a person can grant.ALL_SCOPES: the grantableobject:actionstrings.get_oauth_scopes_supported(): what the OAuth server advertises.
- Frontend type:
APIScopeObjectfrom~/types. - Frontend list at runtime:
Object.values(ScopeObjectEnumApi), fromproducts/access_control/frontend/generated/api.schemas. - Frontend labels and groups:
API_SCOPES,API_SCOPE_GROUPSandgetScopeDescriptionfromlib/scopes. - OAuth lists:
OAUTH_SCOPES_SUPPORTEDandOAUTH_SCOPES_HIDDEN, fromlib/oauthScopes.generatedin the web app andservices/mcp/src/lib/oauth-scopes.generated.tsin the MCP server. The two files are the same.
Signals
- GitHub stars
- 721
- Forks
- 120
- Last commit
- Oct 2026
ahel recommends instead
Advanced
- Item type
- skill
- Key
adding-api-scopes-posthog- Source
- github.com/posthog/posthog-foss
github.com/posthog/posthog-foss