Follow Discord
Sweep 22 Oct 2026 · 04:32Z Build v2.1.308 527 read Stable v2.1.308 Latest v2.1.308 Next v2.1.308 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · claude-code

Verify session identity in self-hosted environments changedself-hosted-environments-identity

Nearest release: v2.1.296, published 6 hours before upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

Upstream edited this page at 9 Oct 2026 23:38 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 20 Oct 2026 18:37 UTC.

Upstream edited
Recorded here
Lines+107added
Lines−111removed
From line 2 where the diff opens
First seen 14 Aug 2026 this site's first read of the page
Recorded edits9to this page, all time

The whole hunk

from line 2, old and new numbered
/
lines
from line 2
22 
33> Verify the CLAUDE_CODE_SESSION_ACCESS_TOKEN JWT so services on your network can trust requests from sessions in your self-hosted environment.
44 
5<Note>
6 Self-hosted environments are in public beta on Team and Enterprise plans; an [Owner](/docs/en/cloud-environments#organization-shared-environments) enables them by turning on **Allow self-hosted environments** on the [**Cloud environments** admin page](https://claude.ai/admin-settings/cloud-environments). This page covers session identity verification; see the [quickstart](/docs/en/self-hosted-environments-quickstart) for setup and [Deploy to production](/docs/en/self-hosted-environments-deploy) for the fleet recipes.
7</Note>
5> [!NOTE]
6>
7> Self-hosted environments are in public beta on Team and Enterprise plans; an [Owner](/docs/en/cloud-environments#organization-shared-environments) enables them by turning on **Allow self-hosted environments** on the [**Cloud environments** admin page](https://claude.ai/admin-settings/cloud-environments). This page covers session identity verification; see the [quickstart](/docs/en/self-hosted-environments-quickstart) for setup and [Deploy to production](/docs/en/self-hosted-environments-deploy) for the fleet recipes.
88 
99A [self-hosted environment](/docs/en/self-hosted-environments) lets Claude Code [cloud sessions](/docs/en/claude-code-on-the-web) run on infrastructure you operate instead of on Anthropic's. Because the session runs inside your network, Claude can call your internal services directly. Those services need a way to confirm that a request came from a Claude Code session in your environment, and to identify the user or service identity that created that session.
1010 
from line 54
5454 
5555Verify each incoming token against these checks:
5656 
57<Steps>
58 <Step title="Check the prefix">
59 Reject the value if it doesn't start with `sk-ant-cc-`, then remove that prefix. The remainder is a standard compact JWT.
60 </Step>
57**1. Check the prefix**
6158 
62 <Step title="Verify the signature">
63 Fetch the JWKS, select the key whose `kid` matches the token header, and verify the `ES256` signature. Reject tokens whose `alg` header is not `ES256`. If a token arrives with a `kid` that isn't in your cached key set, refetch the JWKS once before rejecting it: after a rotation, new tokens are signed with a key your cached set doesn't have yet.
64 </Step>
59Reject the value if it doesn't start with `sk-ant-cc-`, then remove that prefix. The remainder is a standard compact JWT.
6560 
66 <Step title="Verify the issuer">
67 Reject the token if `iss` is not exactly `ccr`.
68 </Step>
61**2. Verify the signature**
6962 
70 <Step title="Verify the audience against your environment">
71 The `aud` claim is an array. Reject the token unless it contains your environment ID, which has the form `ccpool_...`. The environment ID is shown in your environment's detail dialog on the [**Cloud environments** admin page](https://claude.ai/admin-settings/cloud-environments), and appears as the `ccr:pool_id` claim in any of the environment's session tokens. This check is what scopes the token to your environment and rejects tokens issued to other organizations.
72 </Step>
63Fetch the JWKS, select the key whose `kid` matches the token header, and verify the `ES256` signature. Reject tokens whose `alg` header is not `ES256`. If a token arrives with a `kid` that isn't in your cached key set, refetch the JWKS once before rejecting it: after a rotation, new tokens are signed with a key your cached set doesn't have yet.
7364 
74 <Step title="Verify the role">
75 Reject the token if `ccr:role` is not exactly `session_worker`. Other tokens issued for self-hosted environments, such as environment secrets, runner tokens, and work orders, are signed by the same key set but carry different roles.
76 </Step>
65**3. Verify the issuer**
7766 
78 <Step title="Verify expiry">
79 Reject the token if `exp` is in the past. Anthropic issues session tokens with a four-hour lifetime by default and a maximum of eight hours. The runner refreshes the token before expiry and pushes the new value to the session, so subprocesses that Claude starts after a refresh inherit it. One session can therefore present several distinct valid tokens to your service over its lifetime.
80 </Step>
67Reject the token if `iss` is not exactly `ccr`.
8168 
82 <Step title="Read the identity">
83 The creating user's identity is in the `act` claim: `act.sub` is their Anthropic user ID in the prefixed form `user:<id>`, and `act.email`, when the creating surface recorded one, is their email address. Sessions your organization's service identity creates, including Claude Tag channel sessions, carry an `agent:` subject instead, so treat a session as user-created only when `act.sub` carries the `user:` prefix, rather than testing whether identity claims are absent. See the [claims reference](#claims-reference) for the full structure and the flat duplicate claims.
84 </Step>
85</Steps>
69**4. Verify the audience against your environment**
8670 
71The `aud` claim is an array. Reject the token unless it contains your environment ID, which has the form `ccpool_...`. The environment ID is shown in your environment's detail dialog on the [**Cloud environments** admin page](https://claude.ai/admin-settings/cloud-environments), and appears as the `ccr:pool_id` claim in any of the environment's session tokens. This check is what scopes the token to your environment and rejects tokens issued to other organizations.
72 
73**5. Verify the role**
74 
75Reject the token if `ccr:role` is not exactly `session_worker`. Other tokens issued for self-hosted environments, such as environment secrets, runner tokens, and work orders, are signed by the same key set but carry different roles.
76 
77**6. Verify expiry**
78 
79Reject the token if `exp` is in the past. Anthropic issues session tokens with a four-hour lifetime by default and a maximum of eight hours. The runner refreshes the token before expiry and pushes the new value to the session, so subprocesses that Claude starts after a refresh inherit it. One session can therefore present several distinct valid tokens to your service over its lifetime.
80 
81**7. Read the identity**
82 
83The creating user's identity is in the `act` claim: `act.sub` is their Anthropic user ID in the prefixed form `user:<id>`, and `act.email`, when the creating surface recorded one, is their email address. Sessions your organization's service identity creates, including Claude Tag channel sessions, carry an `agent:` subject instead, so treat a session as user-created only when `act.sub` carries the `user:` prefix, rather than testing whether identity claims are absent. See the [claims reference](#claims-reference) for the full structure and the flat duplicate claims.
84 
8785The checks map directly onto standard JWT libraries. The examples below implement the full sequence in Node.js with [`jose`](https://www.npmjs.com/package/jose), which handles JWKS fetching, caching, and `kid` selection, and in Python with [`PyJWT`](https://pyjwt.readthedocs.io/) and its built-in JWKS client.
8886 
89<Tabs>
90 <Tab title="Node.js (jose)">
91 ```typescript theme={null}
92 import { createRemoteJWKSet, jwtVerify } from "jose";
87**Node.js (jose)**
9388 
94 const JWKS = createRemoteJWKSet(
95 new URL("https://api.anthropic.com/v1/code/.well-known/jwks.json")
96 );
89```typescript theme={null}
90import { createRemoteJWKSet, jwtVerify } from "jose";
9791 
98 const PREFIX = "sk-ant-cc-";
99 const EXPECTED_POOL_ID = "ccpool_...";
92const JWKS = createRemoteJWKSet(
93 new URL("https://api.anthropic.com/v1/code/.well-known/jwks.json")
94);
10095 
101 export async function verifySessionToken(raw: string) {
102 if (!raw.startsWith(PREFIX)) {
103 throw new Error("not a self-hosted runner session token");
104 }
105 const jwt = raw.slice(PREFIX.length);
96const PREFIX = "sk-ant-cc-";
97const EXPECTED_POOL_ID = "ccpool_...";
10698 
107 const { payload } = await jwtVerify(jwt, JWKS, {
108 issuer: "ccr",
109 audience: EXPECTED_POOL_ID,
110 algorithms: ["ES256"],
111 });
99export async function verifySessionToken(raw: string) {
100 if (!raw.startsWith(PREFIX)) {
101 throw new Error("not a self-hosted runner session token");
102 }
103 const jwt = raw.slice(PREFIX.length);
112104 
113 if (payload["ccr:role"] !== "session_worker") {
114 throw new Error("token is not a session_worker token");
115 }
105 const { payload } = await jwtVerify(jwt, JWKS, {
106 issuer: "ccr",
107 audience: EXPECTED_POOL_ID,
108 algorithms: ["ES256"],
109 });
116110 
117 const act = payload.act as { email?: string; sub?: string };
118 return {
119 sessionId: payload["ccr:session_id"] as string,
120 poolId: payload["ccr:pool_id"] as string,
121 orgId: payload["ccr:org_id"] as string,
122 creatorEmail: act?.email,
123 creatorSub: act?.sub,
124 };
125 }
126 ```
127 </Tab>
111 if (payload["ccr:role"] !== "session_worker") {
112 throw new Error("token is not a session_worker token");
113 }
128114 
129 <Tab title="Python (PyJWT)">
130 ```python theme={null}
131 import jwt
132 from jwt import PyJWKClient
115 const act = payload.act as { email?: string; sub?: string };
116 return {
117 sessionId: payload["ccr:session_id"] as string,
118 poolId: payload["ccr:pool_id"] as string,
119 orgId: payload["ccr:org_id"] as string,
120 creatorEmail: act?.email,
121 creatorSub: act?.sub,
122 };
123}
124```
133125 
134 JWKS_URL = "https://api.anthropic.com/v1/code/.well-known/jwks.json"
135 PREFIX = "sk-ant-cc-"
136 EXPECTED_POOL_ID = "ccpool_..."
126**Python (PyJWT)**
137127 
138 jwks = PyJWKClient(JWKS_URL)
128```python theme={null}
129import jwt
130from jwt import PyJWKClient
139131 
132JWKS_URL = "https://api.anthropic.com/v1/code/.well-known/jwks.json"
133PREFIX = "sk-ant-cc-"
134EXPECTED_POOL_ID = "ccpool_..."
140135 
141 def verify_session_token(raw: str) -> dict:
142 if not raw.startswith(PREFIX):
143 raise ValueError("not a self-hosted runner session token")
144 token = raw.removeprefix(PREFIX)
136jwks = PyJWKClient(JWKS_URL)
145137 
146 signing_key = jwks.get_signing_key_from_jwt(token)
147 payload = jwt.decode(
148 token,
149 signing_key.key,
150 algorithms=["ES256"],
151 issuer="ccr",
152 audience=EXPECTED_POOL_ID,
153 )
154138 
155 if payload.get("ccr:role") != "session_worker":
156 raise ValueError("token is not a session_worker token")
139def verify_session_token(raw: str) -> dict:
140 if not raw.startswith(PREFIX):
141 raise ValueError("not a self-hosted runner session token")
142 token = raw.removeprefix(PREFIX)
157143 
158 act = payload.get("act") or {}
159 return {
160 "session_id": payload["ccr:session_id"],
161 "pool_id": payload["ccr:pool_id"],
162 "org_id": payload["ccr:org_id"],
163 "creator_email": act.get("email"),
164 "creator_sub": act.get("sub"),
165 }
166 ```
167 </Tab>
168</Tabs>
144 signing_key = jwks.get_signing_key_from_jwt(token)
145 payload = jwt.decode(
146 token,
147 signing_key.key,
148 algorithms=["ES256"],
149 issuer="ccr",
150 audience=EXPECTED_POOL_ID,
151 )
152 
153 if payload.get("ccr:role") != "session_worker":
154 raise ValueError("token is not a session_worker token")
155 
156 act = payload.get("act") or {}
157 return {
158 "session_id": payload["ccr:session_id"],
159 "pool_id": payload["ccr:pool_id"],
160 "org_id": payload["ccr:org_id"],
161 "creator_email": act.get("email"),
162 "creator_sub": act.get("sub"),
163 }
164```
169165 
170166### Verify the token inside the session
171167 
Feedback