Grok Bot Hardening Guide
AI agent hardening for xAI Grok Bot: group-scoped access, connector and egress limits, enforced Auto-review, Team Bot governance, action recording, and hosted-computer lifecycle
Product Editions Covered: Grok Bot on Cursor Enterprise (full admin surface) and self-serve Cursor Teams (partial admin surface). Individual Cursor Pro, Pro+ and Ultra accounts, linked SuperGrok, SuperGrok Plus, SuperGrok Heavy and X Premium+ accounts, and self-serve Grok Business seats have member settings only, apart from Grok Business seat assignment (1.3). Grok Enterprise has no documented admin controls.
Overview
Grok Bot inherits the Cursor team’s org-wide controls, which live in the Cursor Hardening Guide and are not repeated here. Those inherited controls are:
- SSO (cursor 1.3)
- SCIM (cursor 1.4)
- Privacy Mode (cursor 2.1)
- MCP allowlist mechanics (cursor 4.1)
- Cloud Agent settings (cursor 5.3)
- the
*.cursorvm.comand*.*.cursorvm.comproxy allowlist (cursor 9.2) - generic audit-log export (cursor 10.1)
- spend controls (cursor 3.3)
Privacy Mode (Legacy) blocks Grok Bot entirely. The cursor 10.1 pack already pulls every audit event; control 6.2 here covers only the Grok Bot event set.
What Grok Bot is. In xAI’s words, “Grok Bot is a computer-use agent that operates applications, browsers, and development environments. It runs in Cursor’s cloud, and each user’s work executes on a dedicated cloud computer. The desktop and mobile apps are thin clients for chat, review, and approvals” (Grok Bot for teams and enterprises).
- One computer per user: each user gets one persistent computer, a dedicated Firecracker microVM. Every Bot that user runs shares it, including its files, signed-in browser sessions and command-line credentials
- Reach: Bots reach company systems through connectors, which appear as plugins and use the member’s accounts
- Unattended work: Bots keep working while the member’s laptop is closed. They run unattended routines on schedules, Slack triggers and webhooks, and members can tag them on X
- Handoffs: Bots can hand coding work to Cursor Cloud Agents and, with permission, run commands on the member’s own machine
- Team Bots: one Bot, along with its plugins, secrets and team memory, is shared with a whole team and with Slack
Launch and status.
- 2026-08-11: xAI launched Grok Bot with the statement “Grok Bot is in beta and available today for SuperGrok, SuperGrok Plus, and SuperGrok Heavy; Cursor Pro, Pro+, and Ultra; and Cursor Teams Standard and Premium subscribers on desktop and iOS” (Introducing Grok Bot)
- 2026-08-26: xAI wrote that Grok Bot “is now included with all SuperGrok, Cursor Pro, and Cursor Teams plans” (Grok Bot is now included with more plans)
- 2026-09-03: the Enterprise release “adds access, network, and audit controls” (Grok Bot for Enterprise)
- 2026-09-28: Team Bots followed: “Team Bots is available today in public beta on Teams and Enterprise plans” (Team Bots)
The two August posts disagree about which plans were included at launch. Treat Cursor’s plans page as the canonical matrix, except where section 8.6 records that it disagrees with Cursor’s other pages. The current documentation pages, the Admin API’s Grok Bot section and the Organization API’s computer operations carry no beta or GA label.
Who administers it. Grok Bot is an xAI (SpaceXAI) product that runs in Cursor’s cloud on Cursor accounts.
- Cursor holds the product settings: every Grok Bot product setting lives in the Cursor dashboard, the Cursor Admin API and the Cursor Organization API. docs.x.ai mirrors the Grok Bot pages and links back to the Cursor dashboard
- Three levers live elsewhere: Grok Business seat assignment in console.x.ai (1.3), the IdP rules (1.2) and Google Workspace app approval (2.2). Cursor’s own teams page says the same: “A few live in Team Settings, your Team Marketplace, or your identity provider” (Cursor: Grok Bot for teams)
This guide calls the Cursor side “Cursor (the Grok Bot admin plane)”. It is a standalone guide, not a product guide under a platform hub: the Cursor guide covers a different vendor (Anysphere), so this guide cross-references it instead of restructuring it. Grok chat on grok.com and the @grok chatbot on X are different products and out of scope.
How this guide was produced. This guide is AI-drafted from vendor documentation fetched on 2026-10-08, with corrections re-checked against the vendor pages on 2026-10-09. No Cursor dashboard, Grok Bot app or tenant was observed, and nothing here has been applied, validated or reviewed by a person.
Every console path is transcribed from documentation. xAI’s and Cursor’s doc trees sometimes use different labels for the same control. Where they do, the control says so, and section 8.6 lists every conflict to confirm live before rollout.
Editions Covered
| Plan | Grok Bot access | Admin controls |
|---|---|---|
| Cursor Enterprise | Cursor’s plans page says to consult your account executive; the teams page says an admin enables it from the Cursor dashboard. The first enable on an eligible Enterprise team starts a trial (8.6) | Full: enable switch with Manage Group Access, Network Controls, Team Setup and Team Secrets, Allow Local Egress, Action Recording, Enforce Auto-review and Auto-review rules, group Grok Bot tabs, audit logs, OpenTelemetry Export, SCIM, Manage Bot Computers (organization admins) |
| Cursor Teams (self-serve) | On for every member, with no switch to turn it off | Partial: Team Rules, Cloud Agents, public template sharing, the Execution on Local Computer ceiling, connector policy through Team Marketplaces, Manage Team Bots, SSO |
| Cursor Pro / Pro+ / Ultra | Included | Member settings only |
| Linked SuperGrok, SuperGrok Plus, SuperGrok Heavy, X Premium+ | Usage grant on a Cursor account | Member settings only; the link has no documented admin control |
| Grok Business (self-serve) seat | Through Sign in with Grok | Seat assignment in console.x.ai is the only lever (1.3) |
| Grok Enterprise | Through the account manager | None documented |
Grok Bot stays off for teams on Privacy Mode (Legacy) or a legacy request-based plan. Whether Teams-plan Admin API keys can call the Grok Bot write routes is contradictory in Cursor’s own documentation (section 8.6). Appendix A maps every control to the plans that can apply it.
Intended Audience
- Security engineers and GRC teams approving Grok Bot for a Cursor team
- Cursor team and organization admins rolling Grok Bot out
- Identity administrators who run the Okta or Microsoft Entra ID tenant behind Cursor SSO
- Third-party risk managers assessing hosted computer-use agents
How to Use This Guide
- L1 (Crawl): Essential controls for every team that runs Grok Bot
- L2 (Walk): Enhanced controls for security-sensitive environments
- L3 (Run): Strictest options for regulated industries. In this guide they appear as options inside L2 controls, namely Team Allowlist Only in 3.1 and conversation content export in 6.1.
Each control states which plans can apply it. Many are Cursor Enterprise only, and Appendix A summarizes them.
Section 8 is an unleveled reference section. It covers risks with no admin control, documentation conflicts and member-level settings, and it adds no cheat-sheet rows.
How the packs behave. Every API pack verifies first and writes only with enforce --apply, followed by a fresh read.
- Exit 0: no finding in what the API can read. A pack that cannot see part of its control says so on the result line (“NOT proven: …”) instead of printing “compliant”, so exit 0 is not a compliant tenant on its own; the dashboard checks in each control’s Validation steps still apply
- Exit 1: a finding, a failed action, a dry run that would change something, or a write refused after verify had already found the tenant non-compliant
- Exit 2: a precondition; nothing was judged
- Runbook packs: the 7.1 and 7.2 packs are runbook actions, not checks, and their dry runs exit 0
- HTTP 401 and 403: the packs treat both as “not available to this team/plan”, because Cursor’s docs contradict themselves on Admin API reach for Teams (8.6)
Scope
This guide covers:
- the Grok Bot admin controls in the Cursor dashboard, the Cursor Admin API and the Cursor Organization API
- Grok Business seat assignment in console.x.ai
- the IdP sign-in rule the vendor documents for the Bot’s computer browser
- Google Workspace approval of the Grok OAuth app that Grok Bot’s Google plugins sign in as
Cursor-wide controls are in the Cursor guide. Okta, Entra ID, Google Workspace and Slack settings beyond the specific rules named here belong to those products’ own guides. Grok chat, the @grok chatbot on X, the xAI API platform and the Grok Build coding agent are out of scope.
Why Grok Bot Needs Its Own Hardening
Grok Bot meets all three conditions of the lethal trifecta described in the ChatGPT Enterprise guide’s workspace-agent section, and it does so by default:
- Private data comes from the shared signed-in browser, files and connectors
- Untrusted content arrives through web pages, email, Slack messages, X posts and plugin results
- External communication runs through browser navigation, shell network calls, email and Slack sends, and allow-all egress until Network Controls are set
Bots have no identity of their own and act as the signed-in member. The vendor states “Do not use separate Bots as a security boundary” (Approvals, security, and privacy).
This guide’s controls narrow each leg: access (section 1), connectors and reach (2), egress (3), approvals (4), sharing (5), recording (6), and lifecycle and containment (7).
Table of Contents
- Access & Identity
- Agent Reach & Connected Apps
- Hosted Computer & Network
- Autonomy & Approvals
- Sharing & Team Bots
- Monitoring & Audit
- Lifecycle & Containment
- Known Gaps and Member Guidance
- Compliance Quick Reference
1. Access & Identity
1.1 Limit Grok Bot to Approved Groups
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls v8 | 6.1, 6.7 |
| NIST 800-53 | AC-2, AC-3 |
| OWASP Agentic 2026 | ASI03 |
Description
On Cursor Enterprise, turn Grok Bot on with Enable Grok Bot, then use Manage Group Access so only approved groups can run it, never all members by default. On self-serve Cursor Teams, gate it through team membership and assignment to the existing Cursor SSO app.
Enterprise switch: turning the switch off blocks every member without deleting their computers.
Self-serve Teams: the vendor states Grok Bot “is enabled by default, and every member has access” and “There is no switch to turn it off”. The exception is a team on Privacy Mode (Legacy) or a legacy request-based plan, where it stays off. Because “Grok Bot access requires the user to be a member of your Cursor team”, the Teams gate is team membership plus assignment to the existing Cursor SSO app.
Rationale
Why This Matters:
- Each enabled member gets a persistent cloud computer that all of that member’s Bots share, and those Bots act with every account the member signs them into
- Self-serve Teams has no switch, so team membership and IdP assignment are the only gate there
- Terminating computers does not remove a cohort’s access (“None of these remove access”), so group scoping or team removal is the documented way to remove one
Attack Prevented: Agent sprawl across the whole workforce before connector, network and approval controls are in place (OWASP Agentic ASI03 Identity & Privilege Abuse)
Prerequisites
- Enterprise: Cursor Enterprise plan and a team admin. Move off Privacy Mode (Legacy) first, because it “blocks Grok Bot entirely” (cursor 2.1)
- Teams: the existing Cursor SSO application in your IdP (cursor 1.3). SCIM is Enterprise only (cursor 1.4)
ClickOps Implementation
Step 1: Enterprise: enable Grok Bot and scope it to groups
- Navigate to: Cursor dashboard → Grok Bot (https://cursor.com/dashboard/bot)
- Turn on Enable Grok Bot. A setup modal opens covering privacy mode, pricing and model availability
- Select Manage Group Access beside the switch and limit access to the approved groups
Doc conflict: which groups? The API and the Grok Bot doc’s dashboard link point at two different group objects. Confirm live which list the Manage Group Access picker shows.
- Grok Bot doc: links these groups to Members > Groups (https://cursor.com/dashboard/members?subtab=groups)
- Admin API:
PUT /grok-bot/accesswith modelimitedis “for selected billing groups”, withgroup_…ids fromGET /teams/groups- Billing Groups: Enterprise only. A member can be in one at a time, and they do not accept team directory-group ids (
team_group_…). Cursor’s Billing groups page puts them on a different sub-tab, https://cursor.com/dashboard/members?subtab=billing-groups (“under theMembers & Groupstab”)
Step 2: Teams: gate on team membership and SSO assignment
- The Grok Bot page has no enable switch on Teams. Bring members in only through Invite Team on the Grok Bot page or your normal Cursor invitations, and remove anyone who should not run Grok Bot from the team in the dashboard
- Okta: Admin Console → Applications → Applications → open the existing Cursor app → Assignments → Assign → Assign to Groups. With SCIM (Enterprise), assign the same groups to the Cursor SCIM app and push them
- Entra ID: Microsoft Entra admin center → Entra ID → Enterprise apps → All applications → open the existing Cursor enterprise app → Users and groups → Add user/group (Microsoft Learn; the xAI doc still uses the older “Enterprise applications” label). Group-based assignment needs Entra ID P1 or P2, and nested groups are not included
- Do not create a second Grok Bot IdP application. Teams has no SCIM. Without SCIM, unassigning a user in the IdP only blocks SSO sign-in, so the vendor says to remove the user from the team in the dashboard as well
The Okta and Entra steps are transcribed from Configure identity and access.
Code Implementation
The verify region reads GET /grok-bot/access (mode all or limited, plus the groups) and GET /grok-bot/capabilities (enabled). The enforce region lists billing groups with GET /teams/groups, then sends PUT /grok-bot/access with mode limited and 1 to 100 group ids.
- Approved list: billing groups partition the team (“Members can only be in one billing group at a time”, Admin API: billing groups), so a
limitedlist that names every billing group still reaches everyone outside Unassigned. Verify therefore needsHTH_APPROVED_GROUP_IDSto pass alimitedpolicy, and exits 2 (precondition) without it - Teams: mode
allis expected and no API call can change it. SetHTH_PLAN=teamsand the pack reports modeallas not judged by API (exit 2), pointing you to the membership and IdP review - Writes: a Team API key with
admin:*.PUT /grok-bot/accessreturns 400 for an empty limited list, unknown ids, or group ids sent with modeall. Teams-plan reach is contradictory in Cursor’s docs, andGET /grok-bot/capabilitiesis not in the every-plan read list (8.6) - Detection: the Sigma rule alerts on
grok_bot_access_changedwithnew_modeall, and onsand_onboarding(the Grok Bot enable/disable event) withnew_completedtrue. It keys onevent_typeonly, becauseapplication_typenames the acting surface and dashboard or Admin API changes carrycursor, notgrok_bot
Code Pack: API Script
verify() {
echo "Grok Bot 1.1 — who can use Grok Bot"
# `enabled` lives on /capabilities, which is not an every-plan read (TRAP 2).
gb_get team /grok-bot/capabilities
if [ "${GB_CODE}" = "200" ] && jq -e '.enabled | type == "boolean"' "${GB_BODY_FILE}" >/dev/null; then
echo " enabled: $(jq -r '.enabled' "${GB_BODY_FILE}")"
if [ "$(jq -r '.enabled' "${GB_BODY_FILE}")" = "false" ]; then
echo " Grok Bot is disabled for the team: no member has access, whatever the access mode says."
return 0
fi
else
echo " enabled: not readable with this key/plan (HTTP ${GB_CODE}); judging on /access alone"
fi
gb_get team /grok-bot/access
gb_require "GET /grok-bot/access" 200
gb_shape '(.mode | IN("all","limited")) and (.groups | type == "array")' "GET /grok-bot/access"
local mode
mode=$(jq -r '.mode' "${GB_BODY_FILE}")
echo " mode: ${mode}"
jq -r '.groups[] | " - \(.id) \(.name)"' "${GB_BODY_FILE}"
if [ "${mode}" = "all" ]; then
if [ "${HTH_PLAN:-}" = "teams" ]; then
echo "PRECONDITION: mode=all is expected on self-serve Teams, which has no group limit and no switch (TRAP 3) — not judged by API; review the team member list and the IdP's Cursor app assignment instead" >&2
exit 2
fi
gb_finding "Grok Bot access mode is 'all' — every team member can use it. On Enterprise, limit it to approved billing groups; on Teams, gate it through team membership and the IdP's Cursor app assignment, and re-run with HTH_PLAN=teams (TRAP 3)."
return 0
fi
# A limited policy is judged only against the approved list (TRAP 5).
if [ -z "${HTH_APPROVED_GROUP_IDS:-}" ]; then
echo "PRECONDITION: mode is limited; set HTH_APPROVED_GROUP_IDS to judge whether only approved billing groups hold access (billing groups partition every member, so a list naming every billing group reaches everyone outside Unassigned)" >&2
exit 2
fi
local extra
extra=$(jq -r --argjson ok "$(approved_ids_json)" '[.groups[].id] - $ok | .[]' "${GB_BODY_FILE}")
if [ -n "${extra}" ]; then
gb_finding "groups outside HTH_APPROVED_GROUP_IDS have Grok Bot access: $(echo "${extra}" | tr '\n' ' ')"
fi
}
enforce() {
local ids n body unknown
ids=$(approved_ids_json)
n=$(jq 'length' <<<"${ids}")
if [ "${n}" -lt 1 ] || [ "${n}" -gt 100 ]; then
echo "PRECONDITION: HTH_APPROVED_GROUP_IDS must hold 1-100 group ids (it holds ${n}); an empty limited list returns 400" >&2
exit 2
fi
# Every id must be a billing group from List Groups (TRAP 1, TRAP 4).
gb_get team /teams/groups
gb_require "GET /teams/groups" 200
gb_shape '(.groups | type == "array") and all(.groups[]; (.id | type == "string"))' "GET /teams/groups"
unknown=$(jq -r --argjson want "${ids}" '$want - [.groups[].id] | .[]' "${GB_BODY_FILE}")
if [ -n "${unknown}" ]; then
echo "PRECONDITION: not billing-group ids from GET /teams/groups: $(echo "${unknown}" | tr '\n' ' ')(team_group_… directory-group ids are rejected; this pack does not send the reserved unassignedGroup id, which List Groups returns separately)" >&2
exit 2
fi
echo " approved billing groups:"
jq -r --argjson want "${ids}" '.groups[] | select(.id as $i | $want | index($i)) | " - \(.id) \(.name) members=\(.memberCount)"' "${GB_BODY_FILE}"
body=$(jq -nc --argjson g "${ids}" '{mode: "limited", groupIds: $g}')
if [ "${GB_APPLY}" -ne 1 ]; then
gb_plan PUT /grok-bot/access "${body}"
return 0
fi
gb_put team /grok-bot/access "${body}"
gb_require_2xx "PUT /grok-bot/access"
echo " PUT /grok-bot/access -> mode=$(jq -r '.mode // "?"' "${GB_BODY_FILE}") groups=$(jq -c '[.groups[]?.id]' "${GB_BODY_FILE}")"
}
Code Pack: Sigma Detection Rule
# Admin API pull shape (event_type + event_data.<field>). On a stream the fields
# sit under <event_type>.<field>. Never filter on application_type.
detection:
selection_widened:
event_type: 'grok_bot_access_changed'
event_data.new_mode: 'all'
selection_enabled:
event_type: 'sand_onboarding'
event_data.new_completed:
- true
- 'true'
condition: 1 of selection_*
fields:
- timestamp
- user_email
- ip_address
- application_type
- event_type
- event_data.old_mode
- event_data.new_mode
- event_data.old_group_names
- event_data.new_group_names
- event_data.old_completed
- event_data.new_completed
- event_data.source
Validation & Testing
- Enterprise:
GET /grok-bot/accessreturns modelimitedwith only the approved group ids - A test user outside those groups has no Grok Bot access
- The audit log shows
grok_bot_access_changedwithnew_modelimited - Teams: confirm the team member list and the IdP’s Cursor app assignment hold only approved users.
GET /grok-bot/accessis expected to report modeallon Teams; confirm it live (8.6)
Expected result: Only approved groups (Enterprise) or approved team members (Teams) can open Grok Bot.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 6.1 | Establish an Access Granting Process |
| CIS Controls v8 | 6.7 | Centralize Access Control |
| NIST 800-53 | AC-2 | Account Management |
| NIST 800-53 | AC-3 | Access Enforcement |
| SOC 2 | CC6.1 | Logical access security |
| SOC 2 | CC6.2 | User registration and authorization |
| ISO 27001:2022 | A.5.15 | Access control |
| ISO 27001:2022 | A.5.18 | Access rights |
| OWASP Agentic 2026 | ASI03 | Identity & Privilege Abuse |
| Product benchmark | — | No product benchmark exists yet |
1.2 Scope the IdP Sign-In Exception for the Bot’s Computer Browser
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls v8 | 6.3 |
| NIST 800-53 | IA-2, AC-17, AC-20 |
| CISA SCuBA | MS.AAD.3.1v1, MS.AAD.3.7v1 (labeled compatibility exception) |
Description
If Bots must open IdP-provisioned apps from their computer’s browser, add a narrow, labeled sign-in exception scoped to the Cursor-assigned group and to those specific apps. In Entra, start it in report-only.
Why an exception is needed: the hosted computer runs Linux, is not MDM-enrolled (Cursor: Grok Bot deployment), and cannot run Okta FastPass or satisfy compliant-device grants. Under device-trust policies, members therefore cannot sign in to IdP-provisioned apps from the Bot’s browser.
The vendor’s fix:
- Okta: a rule placed above the FastPass, managed-device and deny catch-all rules
- Entra: a new MFA-only policy plus exclusions on the existing blocking policies (xAI Entra steps 3 and 8), because Entra applies every matching policy rather than the highest one (see the doc-conflict callout)
What it does not gate: this rule never gates Grok Bot sign-in or plugin sign-in. A device-aware sign-in policy on the Cursor app itself still gates the member’s own device (okta 1.12).
Rationale
Why This Matters:
- The documented carve-out deliberately relaxes device trust and phishing-resistant MFA for the Bot’s browser
- Microsoft states that the device platform comes from user agent strings and “Because user agent strings can be modified, this information isn’t verified” (Conditional Access conditions). It advises using device platform “with Microsoft Intune device compliance policies or as part of a block statement”
- A broad Linux or Other Desktop allow rule is therefore usable by anyone holding a phished password and a factor that is not phishing-resistant
Attack Prevented: Device-trust bypass by a client spoofing Linux or Other Desktop
Compatibility exception: SCuBA and the vendor disagree.
- SCuBA: CISA SCuBA MS.AAD.3.1v1 says “Phishing-resistant MFA SHALL be enforced for all users.” and MS.AAD.3.7v1 says “Managed devices SHOULD be required for authentication.” (ScubaGear AAD baseline)
- Vendor: its rule relaxes both for the Bot’s browser. For Okta it says “Do not require phishing-resistant or hardware-protection factors” (Configure identity and access)
- This guide: under this repository’s conflict rules, the SCuBA position is the recommendation. Keep phishing-resistant MFA and managed-device requirements in force, and add this exception only if Bots must open IdP-provisioned apps
- Okta risk delta: the exception is one rule. The risk delta is a password plus a non-phishing-resistant factor from any client claiming Other Desktop, limited to the named group and apps
- Entra risk delta: those requirements cannot stay in force on the exception path. The exception works only if the blocking grants are lifted for exactly the group, Linux-claiming clients and the named apps (Step 2a), or, for a phishing-resistant-strength policy alone, if the optional passkey authentication strength in Step 2 satisfies it. The Entra risk delta is therefore the exclusions you record in Step 2a
Doc conflict: Entra policy precedence.
- xAI: its Entra step 3 says “Leave those policies on. Add a higher-priority policy for Grok Bot users on Linux, or exclude them from the blocking policies”, and its FAQ says “laptop sign-ins keep your existing requirements” (Configure identity and access)
- Microsoft: “Multiple Conditional Access policies can apply to an individual user at any time. In this case, all applicable policies must be satisfied” (Building a Conditional Access policy)
Microsoft is authoritative on how Entra evaluates policies. The new MFA policy alone therefore unblocks nothing while a compliant-device, hybrid-join, phishing-resistant-strength or approved-client/app-protection policy covers the same apps.
What admits the Bot’s browser is xAI’s own exclusion: step 3’s “or exclude them from the blocking policies” and step 8’s “On existing compliant-device policies, exclude the group or exclude Linux”. Taken as written, each widens beyond the stated delta:
- Excluding the group drops that grant for its members on every platform and every app the policy covers, laptops included, which contradicts “laptop sign-ins are unchanged”
- Excluding Linux drops it for every user in the policy’s scope whose client claims Linux, and Microsoft says the device platform comes from user agent strings and “this information isn’t verified”
Step 2a below keeps the widening to the group, Linux-claiming clients and the named apps.
Prerequisites
- Okta: Identity Engine for the steps and the Terraform resource (the resource is Identity Engine only). Classic Engine has a variant, noted in Step 1
- Entra ID: rights to create Conditional Access policies
- The list of IdP apps Bots must open from the computer. If the answer is none, do not add the exception
ClickOps Implementation
Step 1: Okta (Identity Engine)
- Navigate to: Admin Console → Security → Authentication Policies → App sign-in. Open the policy attached to the app; find it via Applications → the app → Sign On
- On the Rules tab, select Add rule, and place the rule above the FastPass, managed-device and deny catch-all rules
- IF: group = the Cursor-assigned group, Device platform = Other Desktop, Device state = Any
- THEN: Allowed after successful authentication, with Password + Another factor
- A rule added to a shared policy covers every app on that policy. To scope the rule to specific apps, give those apps their own policy
- Classic Engine: allow Other Desktop without requiring Device Trust = Trusted
Unconfirmed label: “Other Desktop” as an Okta label is sourced only from the xAI doc and from Terraform’s
os_typeOTHER/typeDESKTOP.
- Okta’s own help page (Add an app sign-in policy rule) does not show it
- Okta’s Management API spec (2026.09.2) also lists
LINUXinPolicyPlatformOperatingSystemType, which bears on xAI’s “there is no Linux checkbox”- Confirm live whether the console now offers a Linux platform, and check in the Okta System Log during the pilot whether the Bot’s browser matches Other Desktop or Linux
- Because the vendor says not to require phishing-resistant factors in Okta, any stricter Okta factor is untested by the vendor
Step 2: Microsoft Entra ID
- Navigate to: Microsoft Entra admin center → Entra ID → Conditional Access → Policies → New policy. The xAI doc still uses the older “Protection → Conditional Access” label; Microsoft’s current path is used here (Microsoft Learn)
- Users: the Cursor-assigned group
- Target resources: only the IdP apps the Bot must open. Never select All resources. This is an HTH value, stricter than the vendor’s “Select All resources only if you accept that scope”
- Conditions → Device platforms: include Linux, exclude Windows and macOS
- Grant: Require multifactor authentication. For Entra only, the vendor also suggests an authentication strength that a synced passkey can satisfy, with the passkey installed on the computer by a Team Setup script (3.3, Enterprise only)
- Enable policy: Report-only. The report-only result shows in the sign-in logs even while an existing policy still blocks the sign-in. Switch to On only after reviewing those logs, and before Step 2a, so the exception path is never open without its MFA grant
Step 2a: Entra: lift the blocking grants for exactly the group, Linux and the named apps (HTH pattern)
This split is an HTH construction from Microsoft’s include and exclude rules (“The exclude action overrides the include action in a policy”, Users and groups); the vendor has not tested it. Apply it to each existing policy that applies a compliant-device, hybrid-joined-device, phishing-resistant-strength or approved-client/app-protection grant to the named apps:
- Create copy A: the same grant and the original user scope, Target resources = the named apps, Users → Exclude = the Cursor-assigned group, any device platform. Create it On; while the original policy still covers the named apps it changes nothing
- Create copy B: the same grant, Target resources = the named apps, Users = the Cursor-assigned group, Device platforms = Any device with Linux excluded. Create it On
- Only then, on the original policy, add the named apps under Target resources → Exclude. Microsoft warns that a policy targeting All resources with resource exclusions changes how low-privilege scopes are enforced (“rolling out in phases starting in March, 2026”, Target resources), so check the impact first
- Do not add a Linux block policy for users outside the group. Microsoft lists Linux Desktop with Microsoft Edge as a browser that supports device checks (Conditional Access conditions), so a block would cut off compliant Linux users
The vendor’s simpler options, excluding the group or excluding Linux on each blocking policy, also admit the Bot’s browser but widen as the callout above describes. If you choose one, record it as part of this exception.
Step 3: Keep the member-device gate
- Leave the device-aware sign-in policy on the Cursor app in place (okta 1.12; for Entra authentication strengths see microsoft-entra-id 1.1)
Code Implementation
Cursor and xAI expose no surface for this setting: the Admin API Grok Bot routes have no IdP-policy route. The rule is expressed through the IdP Terraform providers inside this guide’s pack directory, following the precedent of packs/slack/terraform/hth-slack-1.01-okta-saml-config.tf.
The single Terraform file has four regions:
- Providers: pins okta/okta 7.0.0 and hashicorp/azuread 3.10.0. Delete the provider your root module does not use, because
var.idpgates resources, not providers - Okta rule: resolves each app’s policy with the
okta_app_signon_policydata source, which returns only the policy id and name, then creates anokta_app_signon_policy_rulewithgroups_included, platformOTHER/DESKTOP, accessALLOW,factor_mode2FAand a password knowledge constraint.priorityhas no default: the operator supplies a value, or one per policy, that places the rule above the FastPass, managed-device and deny catch-all rules, then confirms the order in the console. Identity Engine only - Okta read-back: reads each rule back with the
okta_app_sign_on_policy_ruledata source, including console-made rules passed inokta_verify_rules. Data-source postconditions failterraform planorapplywhen a rule has no group condition, includes Okta’s Everyone group or a group outside the approved list, matches a platform other than Other Desktop, requires a registered or managed device or a device assurance policy, or is not Allowed with an assurance method. A rule this file creates is read back only during apply, so that failure comes after Okta has written the rule. Two listed apps sharing one policy is a warning only (a Terraformcheckblock, which never fails a run), because xAI allows a shared policy scoped by group. The data source returns no factor mode or constraints, so confirm Password + Another factor in the console - Entra policy: creates an
azuread_conditional_access_policywith Linux included, Windows and macOS excluded, browser clients only, named apps only and MFA, in stateenabledForReportingButNotEnforced, which is report-only. It does not edit or split your existing blocking policies, so applying it alone unblocks nothing in Entra; do Step 2a in the console or in your own azuread code. azuread 3.10.0 has no Conditional Access data source, so Entra verification rests onterraform plandrift plus the Entra sign-in logs
The Okta Policies API and Microsoft Graph Conditional Access routes were not censused for this guide; they belong to the okta and microsoft-entra-id guides.
Code Pack: Terraform
# Both IdPs are declared here because a module may hold only one
# required_providers block. Delete the entry and provider block your root module
# does not use: Terraform configures every declared provider even when var.idp
# leaves it no resources (TRAP 12).
terraform {
# check blocks and cross-variable validation need Terraform 1.9 or later.
required_version = ">= 1.9"
required_providers {
okta = {
source = "okta/okta"
# Arguments transcribed from 7.0.0. Review the schema before raising the pin.
version = "7.0.0"
}
azuread = {
source = "hashicorp/azuread"
# Arguments transcribed from 3.10.0. Review the schema before raising the pin.
version = "3.10.0"
}
}
}
# Reads OKTA_ORG_NAME, OKTA_BASE_URL and the OAuth 2.0 or API-token credential
# from the environment. Identity Engine only.
provider "okta" {}
# Reads ARM_TENANT_ID, ARM_CLIENT_ID and a credential from the environment.
provider "azuread" {}
# Resolve the app sign-in (authentication) policy behind each app the Bot opens.
# xAI: "open the policy attached to the app".
data "okta_app_signon_policy" "bot_app" {
for_each = var.idp == "okta" ? var.okta_app_ids : toset([])
app_id = each.value
}
# The built-in Everyone group, which a "narrow" rule must never include (TRAP 14).
data "okta_everyone_group" "this" {
count = var.idp == "okta" ? 1 : 0
}
locals {
# One rule per distinct policy. Apps that share a policy share its rules (TRAP 5).
okta_policy_ids = toset([for p in data.okta_app_signon_policy.bot_app : p.id])
okta_everyone_group_id = one(data.okta_everyone_group.this[*].id)
okta_approved_groups = coalesce(var.okta_approved_group_ids, var.okta_group_ids)
}
resource "okta_app_signon_policy_rule" "grok_bot_computer" {
for_each = local.okta_policy_ids
policy_id = each.value
name = var.okta_rule_name
# Above the FastPass, managed-device and deny catch-all rules (TRAP 6).
priority = lookup(var.okta_rule_priority_by_policy, each.value, var.okta_rule_priority)
lifecycle {
precondition {
condition = !contains(var.okta_group_ids, local.okta_everyone_group_id)
error_message = "okta_group_ids includes Okta's Everyone group, which makes the exception company-wide. Use the Cursor-assigned group or a pilot subset (TRAP 14)."
}
}
# IF: the Cursor-assigned group, or a pilot subset of it ...
groups_included = var.okta_group_ids
# ... on Device platform = Other Desktop ...
platform_include {
os_type = "OTHER"
type = "DESKTOP"
}
# ... with Device state = Any: device_is_registered, device_is_managed and
# device_assurances_included are deliberately unset (TRAP 8).
# THEN: Allowed after successful authentication, with Password + Another factor.
# No phishing-resistant or hardware-protection constraint (xAI step 5).
access = "ALLOW"
type = "ASSURANCE"
factor_mode = "2FA"
constraints = [
jsonencode({
knowledge = {
types = ["password"]
}
})
]
}
# Read each rule back from Okta: the rules this file manages, plus any rule an
# admin made in the console (var.okta_verify_rules). The data source returns
# conditions and access, not factor mode or constraints (TRAP 13). The
# narrowness asserts are postconditions, so a wide rule FAILS plan or apply; a
# `check` block would only warn.
locals {
okta_rules_to_verify = var.idp != "okta" ? {} : merge(
{
for pid, r in okta_app_signon_policy_rule.grok_bot_computer :
"managed-${pid}" => { policy_id = r.policy_id, rule_id = r.id }
},
var.okta_verify_rules,
)
}
data "okta_app_sign_on_policy_rule" "grok_bot_computer" {
for_each = local.okta_rules_to_verify
id = each.value.rule_id
policy_id = each.value.policy_id
lifecycle {
# A named group, never Everyone, and only approved groups (TRAP 14).
postcondition {
condition = (
length(try(self.conditions.people.groups.include, [])) > 0 &&
!contains(try(self.conditions.people.groups.include, []), local.okta_everyone_group_id) &&
alltrue([
for g in try(self.conditions.people.groups.include, []) :
contains(local.okta_approved_groups, g)
])
)
error_message = "A Grok Bot computer rule has no group condition, includes the Everyone group, or includes a group outside okta_approved_group_ids, so it is wider than the Cursor-assigned group (TRAP 14)."
}
postcondition {
condition = length(try(self.conditions.platform.include, [])) > 0 && alltrue([
for p in try(self.conditions.platform.include, []) :
p.type == "DESKTOP" && try(p.os.type, "") == "OTHER"
])
error_message = "A Grok Bot computer rule matches a platform other than Other Desktop, or carries no platform condition at all."
}
postcondition {
condition = (
!coalesce(try(self.conditions.device.registered, null), false) &&
!coalesce(try(self.conditions.device.managed, null), false) &&
length(coalesce(try(self.conditions.device.assurance.include, null), [])) == 0
)
error_message = "A Grok Bot computer rule requires a registered or managed device or a device assurance policy, which the Bot's computer cannot satisfy (xAI step 4)."
}
postcondition {
condition = (
try(self.actions.app_sign_on.access, "") == "ALLOW" &&
try(self.actions.app_sign_on.verification_method.type, "") == "ASSURANCE"
)
error_message = "A Grok Bot computer rule is not 'Allowed after successful authentication' with an assurance verification method. Check its factor setting in the console (TRAP 13)."
}
}
}
# A warning only: xAI permits a shared policy scoped by group (TRAP 5).
check "okta_listed_apps_do_not_share_a_policy" {
assert {
condition = length(local.okta_policy_ids) == length(data.okta_app_signon_policy.bot_app)
error_message = "Two or more apps in okta_app_ids share one app sign-in policy, so the rule covers every app on that policy. Give the Bot's apps their own policy if that is wider than intended (TRAP 5)."
}
}
# This policy only states what the Bot's browser must satisfy. Entra applies
# every matching policy, so your existing compliant-device, hybrid-join,
# phishing-resistant-strength and approved-client policies still block it until
# you lift them for exactly the group, Linux and these apps (guide 1.2 Step 2a;
# xAI step 8; TRAP 3). This file does not edit those policies.
resource "azuread_conditional_access_policy" "grok_bot_computer_browser" {
count = var.idp == "entra" ? 1 : 0
display_name = var.entra_policy_display_name
# Report-only by default (xAI step 7). Switch to "enabled" after review.
state = var.entra_policy_state
conditions {
# HTH narrowing to the computer browser (TRAP 10).
client_app_types = ["browser"]
# Users: the Cursor-assigned group, or a pilot subset of it.
users {
included_groups = sort(tolist(var.entra_group_object_ids))
}
# Target resources: only the apps the Bot must open, never All (TRAP 11).
applications {
included_applications = sort(tolist(var.entra_application_ids))
}
# Device platforms: include Linux, exclude Windows and macOS (xAI step 5).
platforms {
included_platforms = ["linux"]
excluded_platforms = ["windows", "macOS"]
}
}
# Grant: Require multifactor authentication only (xAI step 6), or the
# passkey-capable authentication strength xAI suggests for Entra (TRAP 9).
grant_controls {
operator = "OR"
built_in_controls = var.entra_authentication_strength_object_id == null ? ["mfa"] : null
authentication_strength_policy_id = (
var.entra_authentication_strength_object_id == null ? null :
"/policies/authenticationStrengthPolicies/${var.entra_authentication_strength_object_id}"
)
}
}
Validation & Testing
- From a pilot Bot computer, sign in to one in-scope app and confirm in the Okta System Log or Entra sign-in logs that the new rule or policy matched. In Entra report-only, the policy shows as report-only success; before Step 2a the sign-in itself is expected to stay blocked by the existing policy
- Try an out-of-scope app and confirm it is still blocked
- Okta: confirm the rule targets only the Cursor-assigned group and the named apps, and that laptop sign-ins are unchanged. Entra: after Step 2a, confirm in the sign-in logs which policies applied, and that a group member’s laptop still gets the device grant through copy B
terraform planshows no drift
Expected result: In Okta, Bots can open only the named IdP apps, and every other sign-in path keeps its existing rules. In Entra, group members on a client claiming Linux reach the named apps with MFA, and all other users, platforms and apps keep their existing grants.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 6.3 | Require MFA for Externally-Exposed Applications |
| NIST 800-53 | IA-2 | Identification and Authentication (Organizational Users) |
| NIST 800-53 | AC-17 | Remote Access |
| NIST 800-53 | AC-20 | Use of External Systems |
| SOC 2 | CC6.1 | Logical access security |
| ISO 27001:2022 | A.5.17 | Authentication information |
| ISO 27001:2022 | A.8.5 | Secure authentication |
| CISA SCuBA | MS.AAD.3.1v1 | Phishing-resistant MFA for all users (labeled compatibility exception) |
| CISA SCuBA | MS.AAD.3.7v1 | Managed devices required for authentication (labeled compatibility exception) |
| Product benchmark | — | No product benchmark exists yet |
1.3 Treat Grok Business Seats as Grok Bot Provisioning
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls v8 | 6.1, 6.2 |
| NIST 800-53 | AC-2, AC-20 |
Description
A self-serve Grok Business seat can unlock Grok Bot without your Cursor team. If you run Grok Business, inventory seat holders and pending invitations, assign seats only to users whose Grok Bot use is acceptable without Cursor-team governance, and unassign on role change.
How the path works: a user can unlock Grok Bot without your Cursor team by choosing Sign in with Grok with a Grok account that holds a seat on a self-serve Grok Business plan. The seat’s license (SuperGrok or SuperGrok Heavy) sets their usage.
Why Cursor controls may not reach it: the console.x.ai admin docs never mention Grok Bot, and no first-party document says Cursor-team controls apply to a user who arrives this way.
Unassigning: it also removes the user’s team-workspace access to Grok; they keep personal workspace functionality.
Rationale
Why This Matters:
- Cursor’s plan page states “If your Grok Business admin removes your seat, or changes its license, Grok Bot usage from the seat ends or changes to match” (Plans and billing), so the seat is the only documented xAI-side lever
- No first-party page says which Cursor team, if any, such a user lands in. Cursor documents domain-verified SSO enforcement (“users on that domain are required to sign in with SSO”, SSO) and an MDM allowed-team-ID policy that logs out other team IDs (Identity and access management). Neither page says how a Sign in with Grok session is treated
Attack Prevented: Shadow Grok Bot use by employees outside the governed Cursor team, with no network policy, no Enforce Auto-review and no audit trail
Prerequisites
- A self-serve Grok Business plan administered in console.x.ai
- The Team Read-Write permission to assign and revoke licenses (purchasing needs Billing Read-Write)
- Personal SuperGrok, SuperGrok Plus, SuperGrok Heavy and X Premium+ links have no documented admin control (section 8.4). Request-based Teams plans and Enterprise seats do not take a personal SuperGrok link
- Grok Enterprise does not qualify for Sign in with Grok; its Grok Bot access goes through the account manager (SuperGrok and Grok Bot)
ClickOps Implementation
Step 1: Inventory seats and invitations
- Navigate to: console.x.ai → overview page
- Export or record the team list with each user’s license
- Review the Pending invitations list. Invite users to Grok Business auto-provisions the selected license type on acceptance when unassigned licenses exist, so a pending invitation is a pending Grok Bot grant
Step 2: Revoke a seat
- In the team list, select … next to the user → Unassign License → confirm
Step 3: Grant a seat (approved users only)
- Select a user from the team list → choose an available license and assign it
Steps are transcribed from Grok Business management.
Automation: ClickOps only — xAI exposes no write interface for this setting (xAI Management API reference, 2026-10-08).
- Management API: it covers auth, billing and audit, and “license” and “seat” appear on none of those pages (billing routes). Its
GET /audit/teams/{teamId}/eventsreturns console team events with a free-form description, and license-assignment events are not documented (audit routes) - SDK and CLI: the xAI SDK, whose source tree has no license-assignment or seat-management module (xai-sdk-python), and the
grokCLI, which is the Grok Build coding agent (CLI reference), have no seat commands either - SCIM: xAI’s SCIM role-to-license provisioning (“Associate the appropriate product license (e.g., Grok Business) with the role”) belongs to console Organizations, and “Organizations are exclusive to the Enterprise tier” (Organization, 2026-10-09). Cursor’s help counts only self-serve Grok Business seats for Sign in with Grok, and says Grok Enterprise does not qualify (SuperGrok and Grok Bot), so that path does not reach the self-serve seats this control covers
Validation & Testing
- Each quarter, reconcile the console.x.ai team list and Pending invitations against the approved Grok Bot roster
- Test with a user who has no other Grok Bot grant (no Cursor plan, no personal link), or a remaining grant will mask the result
- Confirm that after their seat is unassigned, Sign in with Grok no longer grants Grok Bot usage. Propagation time for a seat removal is undocumented, so re-test after a delay
Expected result: Every Grok Business seat holder is on the approved roster, and a revoked seat ends Grok Bot usage.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 6.1 | Establish an Access Granting Process |
| CIS Controls v8 | 6.2 | Establish an Access Revoking Process |
| NIST 800-53 | AC-2 | Account Management |
| NIST 800-53 | AC-20 | Use of External Systems |
| SOC 2 | CC6.2 | User registration and authorization |
| SOC 2 | CC6.3 | Role-based access, modification and removal |
| ISO 27001:2022 | A.5.18 | Access rights |
| ISO 27001:2022 | A.5.23 | Information security for use of cloud services |
| Product benchmark | — | No product benchmark exists yet |
2. Agent Reach & Connected Apps
2.1 Restrict the Connectors Every Bot Inherits
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls v8 | 2.5, 4.8 |
| NIST 800-53 | CM-7, AC-6, SA-9 |
| OWASP Agentic 2026 | ASI02 |
Description
Review the Team Marketplaces before enabling Grok Bot, and block connectors that write to systems of record, send externally or move money unless a named use case needs them. On Enterprise, also maintain the MCP allowlist.
How inheritance works: Grok Bot has no connector list of its own. It inherits the team’s Cursor connector policy on every plan, and “Any permitted connector is available to every Bot a member runs, and a blocked one shows as Disabled by team admin”.
MCP allowlist: its mechanics are in cursor 4.1 and are not repeated here.
Pair with Network Controls: blocking a plugin does not block that service’s website in the Bot’s browser, so pair this control with Network Controls (3.1).
Rationale
Why This Matters:
- Connectors supply the private-data and external-communication legs of Simon Willison’s lethal trifecta, and they do so account-wide
- OAuth tokens stay on Cursor’s connector backend, but the Bot can invoke every tool they authorize
- A Team Bot plugin configured with a key is “The Bot’s own credential, the same for everyone” (Team Bots)
Attack Prevented: Prompt-injected tool misuse through an over-broad connector set (OWASP Agentic ASI02 Tool Misuse)
Real-World Incidents:
-
CVE-2025-54135 (CNA record published 2025-08-05): in Cursor below 1.3.9, an indirect prompt injection could make the agent create a missing MCP settings file such as
.cursor/mcp.jsonwithout user approval and trigger remote code execution. It was a flaw in the Cursor editor’s approvals for workspace files, fixed in 1.3.9, not a Grok Bot flaw.Grok Bot runs on Cursor-hosted computers and inherits the team’s connector policy instead (“Your team’s Cursor MCP (Model Context Protocol) policy applies in full, allowing or blocking each connector”, Cursor: Grok Bot for teams). The CVE is cited here because it shows injected content steering an agent into MCP abuse. That is why the Team Marketplace policy and, on Enterprise, the MCP allowlist (“only servers matching an allowlist entry can run”, Model and integration management) both need tight review.
ClickOps Implementation
Step 1: Set the team’s connector policy
- Navigate to: Cursor dashboard → Plugins & MCPs (https://cursor.com/dashboard/plugins) → Team Marketplaces → open the marketplace
- Set which servers members can use. xAI documents no named per-server allow or block toggle. The levers Cursor documents on that page are Marketplace Settings → Marketplace Access (restrict a whole marketplace to Organization Groups, which needs “A Cursor Enterprise plan with an Organization”), per-plugin installation modes, and removing the plugin (Plugins)
- Block connectors that write to systems of record, send externally or move money unless a named use case needs them
Step 2: Enterprise: maintain the MCP allowlist
- Team dashboard → MCP Configuration (Enterprise only), following cursor 4.1. The Grok Bot doc sends the allowlist to the same place: it points to Cursor’s MCP server trust management section (Model and integration management)
Step 3: Avoid the two traps
- The marketplace page also offers Default On and Required installation modes for Cursor surfaces. xAI says “Pushing connectors to members, whether mandatory or default-on, is not available” for Grok Bot, so do not rely on those modes here
- Cursor warns that “Removing a linked MCP plugin from the marketplace or deleting the marketplace can delete the Team MCP server. This removes it for local users and Cloud Agents. Review the confirmation message before continuing.” (Plugins)
- No Grok-Bot-only lever: Grok Bot “inherits your team’s Cursor connector policy”, and “There is no separate Grok Bot connector list” (Grok Bot for teams and enterprises). Withholding a server from Bots therefore also withholds it from members’ Cursor surfaces
- Enterprise: withhold a server by leaving it off the MCP allowlist (Step 2) instead of removing its plugin: “When an allowlist is active, only servers matching an allowlist entry can run. Servers that don’t match are blocked” (Model and integration management)
- Outside Enterprise: the only per-plugin lever Cursor documents that withholds a server is removal, so read the confirmation message before removing a linked Team MCP plugin
Step 4: If a marketplace is imported from a repository, gate the repository
- A marketplace created with Import from Repo (GitHub, GitLab, Bitbucket or Azure DevOps) is defined by
.cursor-plugin/marketplace.jsonin that repository. With Enable Auto Refresh on (GitHub imports, with the Cursor GitHub App), “Auto Refresh re-reads the full manifest on each push, so new plugins added to the repository are picked up automatically” (Plugins) - A plugin merged to the tracked branch therefore enters the team marketplace with no dashboard step, and Grok Bot inherits that marketplace as its connector policy. Require pull-request review and branch protection on that branch, or leave Auto Refresh off and select Refresh only after review
Doc conflict: labels. The Grok Bot doc’s body says “Team Marketplace” (Grok Bot for teams and enterprises) while its FAQ says “Teams Marketplace”. The Grok Bot changelog entry V0.67.0 says “Marketplace is now Connect Apps”, while the docs still say Marketplace. Confirm the labels live.
Code Implementation
Automation: ClickOps only — Cursor (the Grok Bot admin plane) exposes no write interface for this setting (Admin API, Grok Bot, 2026-10-09). The pack below is read-only verification. Two neighbouring surfaces were ruled out:
- Repository-backed marketplaces: Cursor documents one repository path, the Import from Repo marketplace with Auto Refresh (Step 4). It is a way to add connectors, not to restrict them, so no config pack ships. Cursor documents only that additions are picked up, not whether removing a manifest entry removes the plugin, and the manifest has no access or installation-mode field (Plugins reference, 2026-10-09). Marketplace Access and the installation modes stay in Marketplace Settings
- Editor allowlist: the
~/.cursor/permissions.jsonmcpAllowlistis documented only as “the per-user MCP auto-run allowlist” for the Cursor editor, with no Grok Bot mention
The pack is Enterprise only, because audit logs are an Enterprise feature. For the review window, it pulls from GET /teams/audit-logs:
team_marketplace,mcp_server_configandmcp_authenticationeventsteam_settingsevents, keepingsetting_namevalues beginningmcp_allowlist, because MCP allowlist edits may log there and Cursor calls that list “not an exhaustive list”
mcp_server_config fires for user-scope servers too (scope user or team). Only team-scope rows count as findings; user-scope rows, which are members configuring their own servers, print as context.
- Detection: a Sigma rule alerts on
team_marketplace - Connector calls: with Action Recording on (6.1), every connector call also arrives over OpenTelemetry as
cursor.grok_bot.mcp_tool_call, without tool arguments or results
Code Pack: API Script
verify() {
local days events changes user_cfg auths
days=$(gb_lookback_days 30)
events="$(gb_tmp events)"
gb_audit_pull "team_marketplace,mcp_server_config,mcp_authentication,team_settings" "${days}" "${events}"
GB_UNPROVEN="connector policy set before the ${days}-day window, and the current Team Marketplace and MCP allowlist state (dashboard only, TRAP 4)"
# Team policy changes: marketplace edits, TEAM-scope MCP server configs,
# MCP-allowlist settings (TRAP 1, TRAP 2).
changes=$(jq -c 'select(
.event_type == "team_marketplace"
or (.event_type == "mcp_server_config" and (.event_data.scope // "") == "team")
or (.event_type == "team_settings"
and ((.event_data.setting_name // "") | startswith("mcp_allowlist"))))' "${events}")
user_cfg=$(jq -c 'select(.event_type == "mcp_server_config" and (.event_data.scope // "") != "team")' "${events}")
auths=$(jq -c 'select(.event_type == "mcp_authentication")' "${events}")
echo "Grok Bot 2.1 — connector-policy changes in the last ${days} day(s)"
if [ -n "${changes}" ]; then
jq -r '" \(.timestamp) team=\(.team_id // "-") \(.event_type) by \(.user_email // "unknown") " +
(if .event_type == "mcp_server_config" then
"\(.event_data.action // "?") server=\(.event_data.server_name // "?") type=\(.event_data.server_type // "?") scope=\(.event_data.scope // "?")"
elif .event_type == "team_settings" then
"\(.event_data.setting_name): \(.event_data.old_value | tojson) -> \(.event_data.new_value | tojson)"
else
(.event_data | tojson)
end)' <<<"${changes}"
gb_finding "$(wc -l <<<"${changes}" | tr -d ' ') connector-policy change(s) — map each to an approved request"
else
echo " none"
fi
# Context, not findings: personal MCP servers members configured (TRAP 2),
# and which members connected their accounts to which MCP servers.
echo " user-scope MCP server configs in the window: $(if [ -n "${user_cfg}" ]; then wc -l <<<"${user_cfg}" | tr -d ' '; else echo 0; fi)"
if [ -n "${user_cfg}" ]; then
jq -r '" \(.timestamp) team=\(.team_id // "-") \(.user_email // "unknown") \(.event_data.action // "?") server=\(.event_data.server_name // "?") scope=\(.event_data.scope // "?")"' <<<"${user_cfg}"
fi
echo " MCP authentications in the window: $(if [ -n "${auths}" ]; then wc -l <<<"${auths}" | tr -d ' '; else echo 0; fi)"
if [ -n "${auths}" ]; then
jq -r '" \(.timestamp) team=\(.team_id // "-") \(.user_email // "unknown") \(if (.event_data.action // "") == "" then "authenticate" else .event_data.action end) server=\(.event_data.server_name // "?") scope=\(.event_data.scope // "?")"' <<<"${auths}"
fi
}
Code Pack: Sigma Detection Rule
# Admin API pull shape (event_type + event_data.<field>). The action values for
# team_marketplace are not enumerated in the docs, so every action is selected.
detection:
selection:
event_type: 'team_marketplace'
condition: selection
fields:
- timestamp
- user_email
- ip_address
- event_type
- event_data.action
- event_data.marketplace_id
- event_data.marketplace_name
- event_data.plugin_id
- event_data.plugin_name
Validation & Testing
- A blocked server appears in a member’s Grok Bot plugins as Disabled by team admin
- Enterprise: the read pack lists every
team_marketplace, team-scopemcp_server_configand MCP-allowlistteam_settingschange since the last review, and each maps to an approved request - For each repository-imported marketplace, the tracked branch requires reviewed pull requests
Expected result: Only approved connectors are available to Bots, and every connector-policy change is accounted for.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 2.5 | Allowlist Authorized Software |
| CIS Controls v8 | 4.8 | Uninstall or Disable Unnecessary Services on Enterprise Assets and Software |
| NIST 800-53 | CM-7 | Least Functionality |
| NIST 800-53 | AC-6 | Least Privilege |
| NIST 800-53 | SA-9 | External System Services |
| SOC 2 | CC6.1 | Logical access security |
| SOC 2 | CC9.2 | Vendor and business partner risk management |
| ISO 27001:2022 | A.5.19 | Information security in supplier relationships |
| ISO 27001:2022 | A.5.23 | Information security for use of cloud services |
| OWASP Agentic 2026 | ASI02 | Tool Misuse |
| Product benchmark | — | No product benchmark exists yet |
2.2 Gate Google Plugins by Approving the Grok OAuth App
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls v8 | 2.5, 6.1 |
| NIST 800-53 | AC-3, AC-6, CM-7 |
Description
In Google Workspace, keep the Grok OAuth app unapproved by default and approve it only for the organizational units that hold approved Grok Bot users. Prefer Specific Google data, which allows only the OAuth scopes you list, over Trusted.
Why this is a second gate: Grok Bot’s Gmail, Calendar, Drive, Docs, Sheets and Slides plugins sign in to Google as the OAuth app Grok, not Cursor: “Grok Bot signs in to Google as Grok, not Cursor” (Connect plugins). The Cursor connector policy (2.1) and a Google Workspace app-access decision are therefore two separate gates.
Who is in scope:
- Enterprise:
GET /grok-bot/accessnames the approved groups; expand them with the groups routes or the dashboard to get the member roster - Self-serve Teams: every member has Grok Bot, so the in-scope population is the whole team
Why not Trusted: Google says a trusted app “can request access to all Google data” and that trust “overrides a service restriction”.
Rationale
Why This Matters:
- A Workspace that already reviewed Cursor has not reviewed Grok. An admin searching for Cursor misses the app that actually receives the Google grants for mailbox and Drive reach
- Plugins are account-wide: “An installed plugin is available to every bot on that Grok Bot account”. A teammate’s Team Bot can also use a member’s accounts after an Allow prompt. The tokens stay on Cursor’s connector backend, not on the computer
- Approving Grok does not undo a Cursor block: “It does not turn a Cursor marketplace plugin back on”
Attack Prevented: Unreviewed OAuth grants giving every Bot a member runs mail, calendar and Drive access
Prerequisites
- Grok stays gated only while the Workspace blocks unconfigured third-party apps (API controls → Settings → Unconfigured third-party apps) or restricts Gmail, Drive and Calendar. Google says “Apps that you don’t trust can only access unrestricted services”. Apply google-workspace 3.1 first
- Google’s app-access request feature is not available for Google Workspace for Education
ClickOps Implementation
Step 1: Leave Grok unconfigured
- Do not add Grok under Configured apps → Add app. Google’s documented Add app flow ends “Select Trusted or Blocked and click Configure”, with no scope step, and “A trusted app has access to all Google Workspace services (OAuth scopes), including restricted services” (Google: control third-party app access)
- Do not block it through Add app either. Google counts a blocked app as configured (“Configured apps are apps with an access policy (trusted, limited or blocked)”) and documents the request queue only for “an unconfigured third-party app”
- With unconfigured third-party apps blocked (the prerequisite), have one approved Grok Bot user in each target organizational unit connect a Google plugin. They see Your admin needs to review Grok and “request access if that button is there” (Connect plugins), and Google adds Grok to Apps pending review
Step 2: Approve it for Grok Bot organizational units only
- Navigate to: Google Admin console → Menu → Security → Access and data control → API controls → App access control → Apps pending review → Grok → Configure access
- In the Scope section, select up to 10 organizational units (use bulk updates beyond 10) → Next
- Choose Specific Google data and list only the Gmail, Calendar or Drive scopes the approved plugins need (see Requestable services), plus the Google sign-in scope: “You must include the Google sign-in scope to allow users to sign in with their managed Google Account”. Leaving it out breaks every Grok plugin in a way that looks like the control working
- Select Next → Configure access (Google: review app access requests). Use Trusted only if a documented reason requires it
Alternative when no request exists yet: bulk CSV, per organizational unit
- Manage Third-Party App Access → Bulk update list → Download Blank Template
- Add one row per Grok Bot organizational unit: Type
web application, Id = Grok’s OAuth client ID, Accesstrusted, Org Unit = the unit’s full path. Never use/, which applies the setting “to your entire domain” - Attach CSV File → Upload (Google: add and configure third-party apps in bulk)
- The CSV’s Access column takes only
trusted,blocked,limitedorunconfigured, not Specific Google data, andlimitedreaches only unrestricted services, so it will not unlock Gmail, Drive or Calendar while those services are restricted. Prefer the request path
Cursor’s own help says to add the app named Grok and “Set Grok to Trusted for the people who use Grok Bot” (Connect plugins). This control keeps that intent but scopes it by organizational unit, and prefers Specific Google data.
Gap in Google’s steps: if your console’s Add app flow shows a Scope step, choose the Grok Bot organizational units there and never the top-level unit; Google’s published Add app steps show none.
Automation: ClickOps only — Google Workspace exposes no write interface for per-app trust: the Cloud Identity Policy API’s api_controls settings are “Mutate supported: No” (Supported Policy API settings, 2026-10-08). Cursor and xAI expose none either (Admin API, Grok Bot, 2026-10-08).
- Terraform: the hashicorp/googleworkspace 0.7.0 provider has no third-party app-access resource (registry)
- Read-only: the Policy API can read the prerequisite (
api_controls.unconfigured_third_party_apps,api_controls.google_services), which google-workspace 3.1 covers
Validation & Testing
- Check the prerequisite first. If the Workspace allows unconfigured apps, the test below passes vacuously
- A member outside the trusted OU sees “Your admin needs to review Grok” or “Access blocked” when connecting a Google plugin
- An in-scope member completes Authorize
- Grok does not appear under Configured apps with a domain-wide (
/) Trusted setting
Expected result: Only approved organizational units can connect Grok Bot’s Google plugins.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 2.5 | Allowlist Authorized Software |
| CIS Controls v8 | 6.1 | Establish an Access Granting Process |
| NIST 800-53 | AC-3 | Access Enforcement |
| NIST 800-53 | AC-6 | Least Privilege |
| NIST 800-53 | CM-7 | Least Functionality |
| SOC 2 | CC6.1 | Logical access security |
| ISO 27001:2022 | A.5.15 | Access control |
| ISO 27001:2022 | A.5.23 | Information security for use of cloud services |
| Product benchmark | — | No product benchmark exists yet |
2.3 Disable Cloud Agent Delegation Unless Needed
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls v8 | 4.8 |
| NIST 800-53 | CM-7, AC-6 |
| OWASP Agentic 2026 | ASI02 |
Description
Turn off the team-wide Cloud Agents switch, which is on by default, unless the team has a delegation use case. The switch is available on Teams and Enterprise.
How delegation works: Bots can delegate coding tasks to Cursor Cloud Agents, which run on separate computers under your existing Cloud Agent controls.
Vendor guidance: the vendor’s recommended configuration says “Disable Cloud Agent spawning if you do not need delegation”.
If it stays on: delegated work is governed by the Cloud Agent settings in cursor 5.3, not by this guide’s network policy (“The policy is separate from Cloud Agent network settings”).
Rationale
Why This Matters:
- Delegation extends each Bot’s reach to a second compute surface with its own network and credential settings
- Auto Review judges each Cloud Agent launch, and can deny one, but does not disable the capability; only the team switch does
- On Enterprise, a group’s Grok Bot tab can re-allow Cloud Agents for a cohort (4.3)
Attack Prevented: A hijacked Bot fanning work out beyond the Grok Bot network policy (OWASP Agentic ASI02 Tool Misuse)
ClickOps Implementation
Step 1: Turn delegation off for the team
- Navigate to: Cursor dashboard → Grok Bot page (https://cursor.com/dashboard/bot)
- Under the agent capabilities, set Cloud Agents to off. The switch is set by admins on Teams and Enterprise
Step 2: Enterprise: check group overrides
- Members > Groups → [group] → Grok Bot tab → Agent Capabilities → confirm the group does not allow Cloud Agents (see 4.3). The docs describe this only in prose (“Allow Cloud Agents”), so transcribe the exact toggle label live (8.6)
Code Implementation
- Verify: the pack’s verify region reads
cloudAgentsfromGET /grok-bot/capabilitiesand fails unless it isfalse - Enforce: the enforce region sends
PATCH /grok-bot/capabilitieswith{"cloudAgents": false}. Omitted fields stay unchanged, the call needsadmin:*, and it “Returns 403 when a field is not available to the team” - Plan reach: writes are confirmed for Enterprise; Teams-plan reach is unconfirmed (8.6)
- Group overrides: the read returns the team baseline only; no route returns a group tab’s override
- Evidence of use: with Action Recording on,
cursor.grok_bot.delegationevents (delegation.kindcloud_agent_launchorcloud_agent_followup) record handoffs. A launch Cursor rejected is not recorded
Code Pack: API Script
verify() {
gb_read_capabilities
local caps="${GB_TMP_DIR}/capabilities.json"
if ! jq -e '.cloudAgents | type == "boolean"' "${caps}" >/dev/null; then
echo "PRECONDITION: GET /grok-bot/capabilities has no boolean cloudAgents — nothing was judged" >&2
exit 2
fi
echo "Grok Bot 2.3 — team baseline cloudAgents=$(jq -r '.cloudAgents' "${caps}")"
if [ "$(jq -r '.cloudAgents' "${caps}")" != "false" ]; then
gb_finding "members can delegate Grok Bot work to Cloud Agents (cloudAgents=true)"
fi
echo " (team baseline only — group tabs can re-allow it; see the 4.3 pack)"
GB_UNPROVEN="group Grok Bot tabs that re-allow Cloud Agents (no API reads them; TRAP 1)"
}
enforce() {
local body='{"cloudAgents": false}'
if [ "${GB_APPLY}" -ne 1 ]; then
gb_plan PATCH /grok-bot/capabilities "${body}"
return 0
fi
gb_patch team /grok-bot/capabilities "${body}"
gb_require_2xx "PATCH /grok-bot/capabilities"
echo " PATCH /grok-bot/capabilities -> cloudAgents=$(jq -r '.cloudAgents' "${GB_BODY_FILE}")"
}
Validation & Testing
GET /grok-bot/capabilitiesreturns"cloudAgents": false- Enterprise: confirm in the dashboard that no group’s Grok Bot tab re-allows Cloud Agents, and that no
grok_bot_group_settingsaudit event shows one doing so (4.3)
Expected result: No Bot can launch a Cloud Agent, except for a recorded, approved group exception.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 4.8 | Uninstall or Disable Unnecessary Services on Enterprise Assets and Software |
| NIST 800-53 | CM-7 | Least Functionality |
| NIST 800-53 | AC-6 | Least Privilege |
| SOC 2 | CC6.1 | Logical access security |
| ISO 27001:2022 | A.8.9 | Configuration management |
| OWASP Agentic 2026 | ASI02 | Tool Misuse |
| Product benchmark | — | No product benchmark exists yet |
2.4 Set the Team Execution on Local Computer Ceiling to Never Allow
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls v8 | 4.1, 4.8 |
| NIST 800-53 | AC-6, CM-7, AC-17 |
| OWASP Agentic 2026 | ASI02, ASI05 |
Description
Set the team Execution on Local Computer ceiling to Never allow unless Bots have a specific reason to work on member machines. The control is available on Teams and Enterprise.
What local execution allows: through the desktop app, Bots can “run commands, read files, and move files between the cloud computer and the local machine” (Grok Bot security).
Defaults: the team default, Always allow, “leaves the choice to each member”, and the member’s own setting defaults to asking before every task. “A member’s own setting still applies when it is stricter than the team’s.”
Group overrides (Enterprise): recheck group Grok Bot tabs (4.3), which can raise the ceiling for a cohort (“Group settings only widen”).
Rationale
Why This Matters:
- Local execution carries untrusted cloud-side input onto a managed endpoint that holds corporate credentials and network reach
- The Grok Bot changelog V0.37.0 (September 3, 2026) Fixed list includes “Allow once works even when Execution on this computer is set to Never allow”. That is the member’s own per-computer setting, so at member level a one-time approval still runs
- The team ceiling is the setting to rely on, and its interaction with Allow once must be validated live
Attack Prevented: Prompt-injected commands executing on employee laptops (OWASP Agentic ASI02 Tool Misuse, ASI05 Unexpected Code Execution)
ClickOps Implementation
Step 1: Set the team ceiling
- Navigate to: Cursor dashboard → Grok Bot page (https://cursor.com/dashboard/bot) → Execution on Local Computer
- Select Never allow. The documented options, in order, are Always allow, Ask every time and Never allow
Step 2: Enterprise: check group overrides
- Members > Groups → [group] → Grok Bot tab → Agent Capabilities: confirm no group raises the Execution on Local Computer ceiling (see 4.3)
Doc conflict: member path. The two vendor domains document the member setting at different paths. Confirm the path live.
- xAI: its Approvals, security, and privacy page puts the member setting under Settings → General → Bot → Execution on Local Computer. It then says that once computers are registered, “the choice moves to Settings → Computer → Computers, where each computer has its own Execution on this computer setting”
- Cursor: its Grok Bot security doc writes Settings > Computer > Execution on this computer, with no Computers level
- Per desktop: the setting is per desktop (“Execution on Local Computer is set per computer”, Settings and notifications), so the team ceiling is the only control that covers every member machine
- Lock state: xAI says “If your team’s admin has set a stricter ceiling, Always allow is unavailable”, but it says so in its description of the first-run approval prompt. No doc describes a locked state or lock message in Settings
Code Implementation
- Verify: the verify region reads
localExecutionfromGET /grok-bot/capabilities(never,ask,alwaysornull). It asserts exactlynever, becausenullmeans no ceiling - Enforce: the enforce region sends
PATCH /grok-bot/capabilitieswith{"localExecution": "never"};nullclears the ceiling. Writes are confirmed for Enterprise; Teams-plan writes are unconfirmed (8.6) - Detection: two Sigma rules cover the Enterprise OpenTelemetry stream from Action Recording. One fires on
cursor.grok_bot.shell_commandwithcursor.grok_bot.shell.target=user_machine. The other fires oncursor.grok_bot.file_transferwithcursor.grok_bot.file.target=user_machine, wherefile.directionisread,uploadordownload - Audit: the audit log also records a local computer being registered or renamed (
grok_bot_machine) and a group raising the ceiling (grok_bot_group_settings)
Code Pack: API Script
verify() {
gb_read_capabilities
local caps="${GB_TMP_DIR}/capabilities.json" value
if ! jq -e 'has("localExecution") and ((.localExecution == null) or (.localExecution | IN("never","ask","always")))' "${caps}" >/dev/null; then
echo "PRECONDITION: GET /grok-bot/capabilities has no documented localExecution value — nothing was judged" >&2
exit 2
fi
value=$(jq -r '.localExecution // "null"' "${caps}")
echo "Grok Bot 2.4 — team localExecution ceiling: ${value}"
case "${value}" in
never) ;;
null) gb_finding "no team ceiling (localExecution=null): each member decides whether Bots run commands on their own machine" ;;
*) gb_finding "team ceiling is '${value}', not 'never': Bots can execute on members' machines" ;;
esac
GB_UNPROVEN="group Grok Bot tabs that raise the ceiling (no API reads them; TRAP 3)"
}
enforce() {
local body='{"localExecution": "never"}'
if [ "${GB_APPLY}" -ne 1 ]; then
gb_plan PATCH /grok-bot/capabilities "${body}"
return 0
fi
gb_patch team /grok-bot/capabilities "${body}"
gb_require_2xx "PATCH /grok-bot/capabilities"
echo " PATCH /grok-bot/capabilities -> localExecution=$(jq -r '.localExecution // "null"' "${GB_BODY_FILE}")"
}
Code Pack: Sigma Detection Rules (2)
# The wire reference does not say which OTLP field carries the event name, so
# match the event name or the documented constant body.
detection:
selection_event:
- event_name: 'cursor.grok_bot.shell_command'
- body: 'grok_bot_shell_command'
selection_target:
cursor.grok_bot.shell.target: 'user_machine'
condition: selection_event and selection_target
fields:
- cursor.user.email
- cursor.conversation.id
- cursor.event.id
- cursor.grok_bot.shell.command
- cursor.grok_bot.shell.allowed
- cursor.grok_bot.shell.blocked_reason
- cursor.grok_bot.shell.machine_id
- cursor.grok_bot.shell.exit_code
- cursor.grok_bot.initiated_by
- cursor.grok_bot.provenance
# Direction is seen from the Bot's computer: download = pulled off the member's
# machine, upload = written onto it, read = straight into the Bot's context.
detection:
selection_event:
- event_name: 'cursor.grok_bot.file_transfer'
- body: 'grok_bot_file_transfer'
selection_target:
cursor.grok_bot.file.target: 'user_machine'
condition: selection_event and selection_target
fields:
- cursor.user.email
- cursor.conversation.id
- cursor.event.id
- cursor.grok_bot.file.direction
- cursor.grok_bot.file.outcome
- cursor.grok_bot.file.bytes
- cursor.grok_bot.file.machine_id
- cursor.grok_bot.file.error_category
- cursor.grok_bot.tool_call.id
- cursor.grok_bot.initiated_by
Validation & Testing
GET /grok-bot/capabilitiesreturns"localExecution": "never"- On a member desktop registered under Settings → Computer, confirm that Always allow is unavailable under the team ceiling. Record whether Ask every time stays selectable and what lock text appears, if any; no doc describes either
- Test Allow once against the team ceiling (changelog V0.37.0)
- Enterprise with Action Recording: confirm zero
shell_commandrecords withcursor.grok_bot.shell.allowedtrueand zerofile_transferrecords withcursor.grok_bot.file.outcomesuccess, both with targetuser_machine. Blocked or denied attempts are expected, because these records also cover commands a Bot “was blocked from running” (Wire Reference)
Expected result: No Bot reads, writes or runs anything on a member’s machine.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 4.1 | Establish and Maintain a Secure Configuration Process |
| CIS Controls v8 | 4.8 | Uninstall or Disable Unnecessary Services on Enterprise Assets and Software |
| NIST 800-53 | AC-6 | Least Privilege |
| NIST 800-53 | CM-7 | Least Functionality |
| NIST 800-53 | AC-17 | Remote Access |
| SOC 2 | CC6.1 | Logical access security |
| SOC 2 | CC6.8 | Prevention and detection of unauthorized software |
| ISO 27001:2022 | A.8.1 | User endpoint devices |
| ISO 27001:2022 | A.8.9 | Configuration management |
| OWASP Agentic 2026 | ASI02 | Tool Misuse |
| OWASP Agentic 2026 | ASI05 | Unexpected Code Execution |
| Product benchmark | — | No product benchmark exists yet |
3. Hosted Computer & Network
3.1 Enforce a Destination Allowlist with Network Controls
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls v8 | 9.3, 13.4 |
| NIST 800-53 | SC-7, SC-7(5), AC-4 |
| OWASP Agentic 2026 | ASI01 |
Description
On Enterprise, set Grok Bot Network Access to Defaults + Team Allowlist, or to Team Allowlist Only for regulated work (L3), and turn on Lock for All Groups. Without a policy, Bot egress is allow-all.
Modes:
- Defaults + Team Allowlist permits Cursor’s default destinations plus your list
- Team Allowlist Only permits only your list plus the destinations a computer needs to function
Plans and defaults: “Teams without a policy default to allow-all”, and “Self-serve Teams do not see this panel and cannot set a destination allowlist” (Grok Bot security).
Why lock it: without Lock for All Groups, a group’s own policy “replaces the team’s for their members”.
Applying changes: running computers apply changes within about a minute, and sleeping ones apply them when they next wake; no recreate is needed.
Related settings:
- The policy is separate from Cloud Agent network settings and from connector policy (2.1)
- Allow Local Egress (3.2) is a different concern. The vendor’s private-networks page covers both desktop routing and Team Setup, and it says the network policy “is a separate layer that still applies; private network reach does not replace your destination allowlist” (Connect to private networks)
- Desktop routing still changes the Bot’s source identity and reach: “Destinations see your desktop’s IP address, and the Bot can reach networks available from that device” (Settings and notifications). Turn it off as well (3.2)
Rationale
Why This Matters:
- This is the product’s destination-egress control. Cursor’s security page says “the product control is the destination allowlist rather than a source IP editor” (Cursor: Grok Bot security), and “Dedicated data loss prevention hooks are not available.”
- By default Bot traffic leaves from shared static egress IPs: “The ranges are shared across Grok Bot customers, and dedicated per-customer IPs are not available.” Source-IP trust cannot distinguish your Bots from other customers’ Bots
- The vendor lists the network policy among the controls that do not depend on a model’s judgment, unlike Auto Review (4.1)
Attack Prevented: Exfiltration to arbitrary hosts via URL parameters, shell network calls or downloads after prompt injection (OWASP Agentic ASI01 Agent Goal Hijack)
Real-World Incidents:
- Adversa AI, “Cryptographic Context Injection” (2026-08-20): an encrypted payload on an ordinary-looking web page made xAI Grok web chat (its agentic browsing framework) decrypt it and open a URL carrying the user’s prompts. The attack was still reproducible as of August 19. It was demonstrated on Grok chat, not Grok Bot; an egress allowlist blocks the exfiltration host either way (Adversa AI)
- Radware ShadowLeak (2025-09-18, Zvika Babo and Gabi Nakibly): zero-click indirect prompt injection made ChatGPT Deep Research leak inbox data “directly from OpenAI’s cloud infrastructure, making it invisible to local or enterprise defenses”. Grok Bot computers occupy the same service-side egress position (Radware; the host serves a bot wall to automated fetchers and was read in a real browser on 2026-10-08)
Prerequisites
- Cursor Enterprise. The panel does not exist on self-serve Teams
- A reviewed destination list (domains, wildcard domains, IP addresses, CIDR ranges, and
host-or-CIDR:portentries for raw connections)
ClickOps Implementation
Step 1: Set the team policy
- Navigate to: Cursor dashboard → Grok Bot page (https://cursor.com/dashboard/bot) → section Network → control Grok Bot Network Access
- Choose Defaults + Team Allowlist (L2) or Team Allowlist Only (L3). The other modes are No Policy (Allow All) and Allow All Network Access
- Add destinations: domains and IP ranges, with ports for raw connections
- Turn on Lock for All Groups, so that a group’s Group Network Access cannot replace the team policy
Step 2: Turn off desktop routing
- Turn off Allow Local Egress (3.2)
The section and control labels (Network, Grok Bot Network Access, Lock for All Groups, Group Network Access) appear only on Grok Bot for teams and enterprises. The four mode labels also appear on docs.x.ai’s Grok Bot security page. The cursor.com copies, and docs.x.ai’s private-networks page, write the modes in sentence case (“Defaults plus team allowlist”, “Team allowlist only”).
Doc conflict: allowlist size. The security page says there is “no cap on the number of entries”. The Admin API’s
PUT /grok-bot/networkaccepts “Up to 500 destinations, 1 to 253 characters each”. The pack enforces the API limit.
Code Implementation
- Read:
GET /grok-bot/networkreturnsegressMode(unset,allow_all,default_with_network_settingsornetwork_settings_only),allowlistandlocked. It is documented as readable on every plan, so a Teams admin can prove the policy is unset, which means allow-all - Write:
PUT /grok-bot/networkis a full replace. All three fields are required, partial bodies, unknown modes and invalid entries return 400, and it “Returns 403 on Teams plans”
What the pack does:
- Verify reads the policy on any plan. It flags a mode that restricts nothing,
lockedfalse, and catch-all entries (0.0.0.0/0,::/0or a bare*, with or without a port) that make a restrictive mode allow-all in practice - Enforce PUTs a reviewed allowlist file with
lockedset totrueand theegressModenamed byHTH_EGRESS_MODE:default_with_network_settings(“Cursor defaults plus the allowlist”) for L2 ornetwork_settings_only(“The allowlist and destinations required to run Grok Bot”) for L3 (Admin API: replace network policy). It rejects files of more than 500 entries - Mode choice: because the PUT is a full replace, enforce never picks L3 on its own. Without
HTH_EGRESS_MODEit keeps a live restrictive mode and otherwise sendsdefault_with_network_settings - Audit: no Grok-Bot-specific event names the team network policy.
team_settingsfires for team-wide changes “including changes made through the Admin API” and may carry it, but itssetting_namevalues are not enumerated, so this is unverified. No event records an allowlist block.grok_bot_group_settingsplausibly records Group Network Access changes, with the same caveat - Evidence of use: with Action Recording and OpenTelemetry Export on,
cursor.grok_bot.browser_navigation(cursor.grok_bot.browser.url, normalized toscheme://host/path) gives after-the-fact destination evidence for browser navigation only
Code Pack: API Script
verify() {
gb_get team /grok-bot/network
gb_require "GET /grok-bot/network" 200
gb_shape '(.egressMode | IN("unset","allow_all","default_with_network_settings","network_settings_only"))
and (.allowlist | type == "array") and (.locked | type == "boolean")' "GET /grok-bot/network"
local live mode locked want drift catchall
live="${GB_TMP_DIR}/network.json"
cp "${GB_BODY_FILE}" "${live}"
mode=$(jq -r '.egressMode' "${live}")
locked=$(jq -r '.locked' "${live}")
echo "Grok Bot 3.1 — egressMode=${mode} locked=${locked} allowlist=$(jq '.allowlist | length' "${live}") entr(ies)"
jq -r '.allowlist[] | " - \(.)"' "${live}"
case "${mode}" in
default_with_network_settings|network_settings_only) ;;
*) gb_finding "egressMode=${mode}: Bots can reach any destination (TRAP 3)" ;;
esac
if [ "${locked}" != "true" ]; then
gb_finding "locked=false: a group's own network policy replaces the team policy for its members"
fi
if [ -n "${HTH_EGRESS_MODE:-}" ] && [ "${mode}" != "${HTH_EGRESS_MODE}" ]; then
gb_finding "egressMode=${mode}, expected ${HTH_EGRESS_MODE}"
fi
# Catch-all entries defeat a restrictive mode (TRAP 5).
catchall=$(jq -r '.allowlist[] | select(test("^(0\\.0\\.0\\.0/0|::/0|\\*)(:[0-9]+)?$")) | " catch-all: \(.)"' "${live}")
if [ -n "${catchall}" ]; then
echo "${catchall}"
gb_finding "the allowlist holds catch-all entries, which allow every destination they match (TRAP 5)"
fi
# Drift against the reviewed file, when one is supplied.
if [ -n "${HTH_ALLOWLIST_FILE:-}" ]; then
want=$(reviewed_allowlist)
drift=$(jq -r --argjson want "${want}" '
((.allowlist - $want) | map(" + live, not reviewed: \(.)")) +
(($want - .allowlist) | map(" - reviewed, not live: \(.)")) | .[]' "${live}")
if [ -n "${drift}" ]; then
echo "${drift}"
gb_finding "the live allowlist differs from ${HTH_ALLOWLIST_FILE}"
fi
fi
}
enforce() {
local want mode live_mode body n bad
want=$(reviewed_allowlist)
# TRAP 6: explicit choice, else keep the live restrictive mode, else L2.
live_mode=$(jq -r '.egressMode' "${GB_TMP_DIR}/network.json")
if [ -n "${HTH_EGRESS_MODE:-}" ]; then
mode="${HTH_EGRESS_MODE}"
else
case "${live_mode}" in
default_with_network_settings|network_settings_only) mode="${live_mode}" ;;
*) mode="default_with_network_settings" ;;
esac
echo " egressMode: ${mode} (HTH_EGRESS_MODE unset; live mode is ${live_mode}$(if [ "${mode}" != "network_settings_only" ]; then echo "; set HTH_EGRESS_MODE=network_settings_only for L3"; fi))"
fi
n=$(jq 'length' <<<"${want}")
if [ "${n}" -gt 500 ]; then
echo "PRECONDITION: ${n} destinations exceed the Admin API's documented 500 (TRAP 2)" >&2
exit 2
fi
bad=$(jq -r '.[] | select(length > 253 or test("\\s") or test("^(0\\.0\\.0\\.0/0|::/0|\\*)(:[0-9]+)?$")) | " \(.)"' <<<"${want}")
if [ -n "${bad}" ]; then
echo "PRECONDITION: entries longer than 253 characters, containing whitespace, or catch-all (TRAP 5):" >&2
echo "${bad}" >&2
exit 2
fi
if [ "${n}" -eq 0 ]; then
echo " WARNING: the reviewed file holds zero destinations"
fi
# TRAP 1: always the whole policy, always locked.
body=$(jq -nc --arg m "${mode}" --argjson a "${want}" '{egressMode: $m, allowlist: $a, locked: true}')
if [ "${GB_APPLY}" -ne 1 ]; then
gb_plan PUT /grok-bot/network "${body}"
return 0
fi
gb_put team /grok-bot/network "${body}"
gb_require_2xx "PUT /grok-bot/network"
echo " PUT /grok-bot/network -> egressMode=$(jq -r '.egressMode' "${GB_BODY_FILE}") locked=$(jq -r '.locked' "${GB_BODY_FILE}") allowlist=$(jq '.allowlist | length' "${GB_BODY_FILE}")"
}
Validation & Testing
GET /grok-bot/networkreturnsegressModenetwork_settings_only(ordefault_with_network_settings) with"locked": true- Ask a pilot Bot to fetch a host that is not on the allowlist and confirm it fails
Expected result: Bot computers reach only allowlisted destinations, and no group can replace the team policy.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 9.3 | Maintain and Update Network-Based URL Filters |
| CIS Controls v8 | 13.4 | Perform Traffic Filtering Between Network Segments |
| NIST 800-53 | SC-7 | Boundary Protection |
| NIST 800-53 | SC-7(5) | Deny by Default — Allow by Exception |
| NIST 800-53 | AC-4 | Information Flow Enforcement |
| SOC 2 | CC6.6 | Logical access security against threats from outside system boundaries |
| ISO 27001:2022 | A.8.20 | Networks security |
| ISO 27001:2022 | A.8.22 | Segregation of networks |
| OWASP Agentic 2026 | ASI01 | Agent Goal Hijack |
| Product benchmark | — | No product benchmark exists yet |
3.2 Turn Off Allow Local Egress
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls v8 | 12.2, 13.4 |
| NIST 800-53 | SC-7, AC-4, AC-17 |
Description
Turn off Allow Local Egress for the team, and confirm that no group’s Grok Bot tab turns it back on. Enterprise only.
What it does: Allow Local Egress lets members route the cloud computer’s web traffic through their own desktop. Destinations then see the desktop’s IP address, and the Bot can reach networks available from that device.
Turning it off: “Turning it off stops active routes within five minutes”, and turning it back on restores each member’s previous choice.
Group check: a group’s Grok Bot tab can still “turn on Allow Local Egress for group members when the team has them off”, and a member in several groups gets the most permissive result, so the group check is part of this control.
Private-network access: if a cohort needs it, prefer a Team Setup networking client with a scoped key (3.3) over routing through laptops.
Rationale
Why This Matters:
- Routing through a corporate laptop gives the Bot the laptop’s source IP and every network that device reaches
- IP-based trust and internal reach therefore extend to an agent driven by untrusted input
- The vendor notes Network Controls “is a separate layer that still applies; private network reach does not replace your destination allowlist” (Connect to private networks), so this control is about reach and source identity, not about bypassing 3.1
Attack Prevented: Lateral movement from a hijacked Bot into internal networks reachable from member devices
ClickOps Implementation
Step 1: Turn the team switch off
- Navigate to: Cursor dashboard → Grok Bot page (https://cursor.com/dashboard/bot) → Allow Local Egress → Off (it is on by default)
Step 2: Check every group
- Members > Groups (https://cursor.com/dashboard/members?subtab=groups) → each team-owned group → Grok Bot tab → Agent Capabilities
- Confirm Allow Local Egress is not turned on for the group
Doc conflict: member labels. The two vendor domains label the member-side toggle and its locked message differently.
- Toggle: the member-side toggle this locks is in the Grok Bot desktop app under Settings → Computer → Network → Route traffic through this computer. docs.x.ai’s settings page labels it “Route egress through this desktop” instead
- Locked message: “Disabled by your admin. You can’t turn this on.” on cursor.com (Cursor: Grok Bot settings), and “Your team’s admin has turned off local egress.” on docs.x.ai (Settings and notifications)
Code Implementation
- Verify: the verify region reads
localEgressAllowedfromGET /grok-bot/capabilities - Enforce: the enforce region sends
PATCH /grok-bot/capabilitieswith{"localEgressAllowed": false}. The Admin API calls this “Allow Local Egress Routing” and marks it Enterprise only. It needsadmin:*, an empty PATCH returns 400, and the field “Returns 403 when local egress routing controls are not enabled for the team” - Group overrides: the read returns the team baseline only, so a group re-enabling local egress is invisible to it. The 4.3 pack flags group-setting changes inside its audit window; only the dashboard review in Step 2 sees overrides set earlier
- Audit: no audit event names the team toggle.
grok_bot_group_settingsrecords a group re-enabling it, andteam_settingsmay carry the team toggle, but this is unverified because itssetting_namelist is non-exhaustive
Code Pack: API Script
verify() {
gb_read_capabilities
local caps="${GB_TMP_DIR}/capabilities.json"
if ! jq -e '.localEgressAllowed | type == "boolean"' "${caps}" >/dev/null; then
echo "PRECONDITION: GET /grok-bot/capabilities has no boolean localEgressAllowed — Allow Local Egress Routing is Enterprise only; nothing was judged" >&2
exit 2
fi
echo "Grok Bot 3.2 — team baseline localEgressAllowed=$(jq -r '.localEgressAllowed' "${caps}")"
if [ "$(jq -r '.localEgressAllowed' "${caps}")" != "false" ]; then
gb_finding "members can route Grok Bot's web traffic through their own computer, so destinations see that device's IP address and the Bot can reach networks available from it"
fi
echo " (team baseline only — group tabs can re-enable it; see the 4.3 pack)"
GB_UNPROVEN="group Grok Bot tabs that re-enable Allow Local Egress (no API reads them; TRAP 2)"
}
enforce() {
local body='{"localEgressAllowed": false}'
if [ "${GB_APPLY}" -ne 1 ]; then
gb_plan PATCH /grok-bot/capabilities "${body}"
return 0
fi
gb_patch team /grok-bot/capabilities "${body}"
gb_require_2xx "PATCH /grok-bot/capabilities"
echo " PATCH /grok-bot/capabilities -> localEgressAllowed=$(jq -r '.localEgressAllowed' "${GB_BODY_FILE}")"
}
Validation & Testing
GET /grok-bot/capabilitiesreturns"localEgressAllowed": false- Every group’s Grok Bot tab leaves Allow Local Egress off
- On a member desktop, the toggle shows as locked with one of the two documented messages
Expected result: No member can route Bot traffic through their own device.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 12.2 | Establish and Maintain a Secure Network Architecture |
| CIS Controls v8 | 13.4 | Perform Traffic Filtering Between Network Segments |
| NIST 800-53 | SC-7 | Boundary Protection |
| NIST 800-53 | AC-4 | Information Flow Enforcement |
| NIST 800-53 | AC-17 | Remote Access |
| SOC 2 | CC6.6 | Logical access security against threats from outside system boundaries |
| ISO 27001:2022 | A.8.20 | Networks security |
| ISO 27001:2022 | A.8.22 | Segregation of networks |
| Product benchmark | — | No product benchmark exists yet |
3.3 Govern Team Setup Scripts and Keep Credentials in Team Secrets
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls v8 | 2.7, 4.1, 10.1 |
| NIST 800-53 | CM-3, CM-5, SI-4, IA-5(7) |
Description
Treat Team Setup manifests and Group Setup Scripts as fleet-wide privileged code: put two-person change control around them, keep credentials in Team Secrets rather than in scripts, and scope any shared networking-client auth key to the minimum hosts. Enterprise only.
How Team Setup runs:
- Manifests run as the computer user, with sudo available, on every team computer
- They run at start and on a roughly daily refresh, with a 30-minute timeout per script, and entries run in order
- They are the Enterprise path to install your own EDR, networking clients (Tailscale, Cloudflare Tunnel) and password managers, because Grok Bot “does not ship a built-in customer-facing telemetry or EDR feed” (Grok Bot security)
- Group Setup Scripts run alongside Team Setup and read the same Team Secrets; there are no group-level secrets
Change control: the vendor documents no approval workflow on manifest saves, so two-person change control is an HTH process recommendation.
Secrets: never paste secret values into scripts; store credentials as Team Secrets.
Shared auth keys: every computer joins under the same key and tags, which is why the key must be scoped to the minimum hosts.
Rationale
Why This Matters:
- One manifest change executes on every member’s computer. Manifests are “plain text applied to your whole fleet”, and members can review the managed setup under Team Setup in the app (Connect to private networks)
- Team Secrets are encrypted and never shown again. They exist only in Setup and Check Script processes, they are redacted from output, and they are not delivered when a computer takes setup from two or more teams
- Team admins, including unpaid admins, can add, replace or delete Team Secrets, and no audit event is documented for those changes
Attack Prevented: A malicious or credential-leaking setup script rolled out to the whole fleet, and standing secrets kept in plain text
Real-World Incidents:
- xAI developer API key leaked on GitHub (KrebsOnSecurity, 2025-05-01): GitGuardian found that the key had access to “at least 60 fine-tuned and private LLMs”, including several unreleased Grok models. GitGuardian alerted the employee on March 2, and the key was still valid on April 30. This is the plain-text-secret failure mode that Team Secrets exists to avoid (KrebsOnSecurity)
Prerequisites
- Cursor Enterprise
- A change-control process (two-person review) for manifest and Setup Script edits
- Limits: up to 100 manifests per team; up to 100 secrets per team, 32 KB per value and 96 KB in total
ClickOps Implementation
Step 1: Author or review a team manifest
- Navigate to: Cursor dashboard → Grok Bot → Team Setup → Manifests → + (New Manifest) → Manifest ID
- Add each script entry (ID, Setup Script, optional Check Script); a Form/JSON toggle sits at the top of the editor → Save
- Cohort-only scripts live under Members > Groups → [group] → Grok Bot tab → Setup Scripts. Review them on the same schedule
Step 2: Move credentials into Team Secrets
- Cursor dashboard → Grok Bot → Team Secrets → add the environment variable name and value
- Reference the secret by name from the script’s environment, and remove any literal value from manifest text
Step 3: Roll out
- Organization admins (Enterprise): Grok Bot → Bot Computers → Manage Bot Computers → Manage → select members → Manage selected → Recreate VMs → confirm Recreate VMs
- Otherwise a member reset or the roughly daily refresh picks up changes
- If the Grok Bot page shows the banner “Restart VMs to apply changed settings”, team admins can select Restart all VMs → Recreate VMs. The banner is documented only as appearing “after some changes to team settings”
- A recreate removes apps members installed themselves, and sign-in sessions, including the networking client’s login, may need re-establishing
Code Implementation
- Read:
GET /grok-bot/setup-manifestslists team manifests (id, andscriptswithid,setupandcheck), paged at 50 by default and 100 at most - Write:
PUT /grok-bot/setup-manifests/:manifestIdupserts one, andDELETEremoves it. A PUT returns 409 when the manifest changed during the request, and 403 when the feature is not available to the team or its plan
What the pack does:
- Verify lists team manifests and flags credential-shaped literals in setup and check text:
- AWS key ids, PEM private keys, GitHub and Slack tokens
- Tailscale
tskey-auth keys and Cloudflare Accesscfast_client secrets - credentials in URLs,
key=valueassignments and literal Authorization headers - secret flags passed as a separate argument, such as
tailscale up --auth-key tskey-…orcloudflared access tcp --service-token-secret <value>
- Verify results: a reference to an environment variable (
--auth-key "$TS_AUTHKEY") passes. Matches are reported by pattern name only, and a clean scan is not proof of absence - Enforce upserts a reviewed manifest file only when it is missing or differs from the live copy, and refuses a file that itself carries a credential-shaped literal. Its dry run prints each script’s id and length, never the script text, so a literal the scan missed never lands in a CI log
- Detection: a Sigma rule alerts on
grok_bot_team_setup_manifest(save or delete, with revision, entry count and entry ids) and ongrok_bot_group_resourcewith resourcesetup_manifest. Script text is never logged. Recreates are audited asgrok_bot_vmforce_recreateandgrok_bot_vm_bulkbulk_recreate
Automation: Team Secrets and group Setup Scripts are ClickOps only — Cursor (the Grok Bot admin plane) exposes no write interface for them. Team Secrets “are not available through the Admin API” and “are managed from the dashboard only” (Grok Bot for teams and enterprises, 2026-10-08), and group Setup Scripts have no route (Admin API, Grok Bot, 2026-10-08).
No audit event is documented for Team Secrets add, replace or delete; cloud_agent_secret covers Cloud Agent secrets only.
Code Pack: API Script
verify() {
local manifests hits unreviewed want live
manifests=$(list_manifests)
LIVE_MANIFESTS="${manifests}"
echo "Grok Bot 3.3 — team setup manifests: $(jq 'length' <<<"${manifests}")"
jq -r '.[] | " - \(.id): \([.scripts[].id] | join(", "))"' <<<"${manifests}"
# Credential-shaped literals in setup or check text (TRAP 3). Names only.
hits=$(jq -r "${CRED_SCAN}"'
.[] | .id as $m | .scripts[]
| .id as $s | ([(.setup // ""), (.check // "")] | map(cred_hits) | add | unique) as $h
| select($h | length > 0)
| " \($m)/\($s): \($h | join(", "))"' <<<"${manifests}")
if [ -n "${hits}" ]; then
echo "${hits}"
gb_finding "credential-shaped literals in team setup scripts — move them to Team Secrets and reference them by name"
fi
if [ -n "${HTH_REVIEWED_MANIFEST_IDS:-}" ]; then
unreviewed=$(jq -r --arg ok "${HTH_REVIEWED_MANIFEST_IDS}" \
'($ok | split(",") | map(gsub("^\\s+|\\s+$"; ""))) as $r | .[] | select(.id as $i | $r | index($i) | not) | .id' <<<"${manifests}")
if [ -n "${unreviewed}" ]; then
gb_finding "manifests outside HTH_REVIEWED_MANIFEST_IDS: $(echo "${unreviewed}" | tr '\n' ' ')"
fi
fi
if [ -n "${HTH_MANIFEST_FILE:-}" ]; then
want=$(reviewed_manifest)
live=$(jq -c --arg id "$(jq -r '.id' <<<"${want}")" '[.[] | select(.id == $id)] | first // null' <<<"${manifests}")
if [ "${live}" = "null" ]; then
gb_finding "reviewed manifest '$(jq -r '.id' <<<"${want}")' is not deployed"
elif [ "$(manifest_scripts <<<"${live}")" != "$(manifest_scripts <<<"${want}")" ]; then
gb_finding "live manifest '$(jq -r '.id' <<<"${want}")' differs from ${HTH_MANIFEST_FILE}"
fi
fi
echo " NOT VERIFIABLE BY API: Team Secrets and group Setup Scripts (dashboard only, TRAP 2)"
GB_UNPROVEN="Team Secrets and group Setup Scripts (dashboard only, TRAP 2)"
}
enforce() {
local want id body hits live
want=$(reviewed_manifest)
id=$(jq -r '.id' <<<"${want}")
# Never upload a literal secret: that is the failure this control exists to stop.
hits=$(jq -r "${CRED_SCAN}"'.scripts[] | .id as $s
| ([(.setup // ""), (.check // "")] | map(cred_hits) | add | unique) as $h
| select($h | length > 0) | " \($s): \($h | join(", "))"' <<<"${want}")
if [ -n "${hits}" ]; then
echo "${hits}" >&2
echo "PRECONDITION: ${HTH_MANIFEST_FILE} carries credential-shaped literals — refusing to upload it" >&2
exit 2
fi
# Upload only when the reviewed manifest is missing or differs: a no-op save
# bumps the revision, fires the 3.3 Sigma rule and cannot clear any other finding.
live=$(jq -c --arg id "${id}" '[.[] | select(.id == $id)] | first // null' <<<"${LIVE_MANIFESTS}")
if [ "${live}" != "null" ] && [ "$(manifest_scripts <<<"${live}")" = "$(manifest_scripts <<<"${want}")" ]; then
echo " reviewed manifest '${id}' already matches the live copy — nothing to upload."
echo " The remaining finding(s) need manual remediation: fix the flagged manifests, or remove manifests outside HTH_REVIEWED_MANIFEST_IDS, in the dashboard or in their own reviewed files."
return 0
fi
body=$(jq -c '{scripts}' <<<"${want}")
if [ "${GB_APPLY}" -ne 1 ]; then
# Script text is never printed, so a literal the scan missed stays out of CI logs (TRAP 3).
gb_plan PUT "/grok-bot/setup-manifests/${id}"
jq -r '.scripts[] | " \(.id): setup \(.setup | length) chars, check \((.check // "") | length) chars"' <<<"${want}"
return 0
fi
gb_put team "/grok-bot/setup-manifests/${id}" "${body}"
gb_require_2xx "PUT /grok-bot/setup-manifests/${id}"
echo " PUT /grok-bot/setup-manifests/${id} -> scripts=$(jq -c '[.manifest.scripts[]?.id]' "${GB_BODY_FILE}")"
echo " Running computers pick up manifest changes on a periodic refresh, roughly daily;"
echo " recreate a computer (or have the member reset it) to apply the change immediately."
}
Code Pack: Sigma Detection Rule
# Admin API pull shape (event_type + event_data.<field>). Script text is never
# logged; fetch the manifest itself to see what changed.
detection:
selection_team_manifest:
event_type: 'grok_bot_team_setup_manifest'
selection_group_script:
event_type: 'grok_bot_group_resource'
event_data.resource: 'setup_manifest'
condition: 1 of selection_*
fields:
- timestamp
- user_email
- ip_address
- event_type
- event_data.action
- event_data.manifest_id
- event_data.revision
- event_data.entry_count
- event_data.entry_ids
- event_data.group_id
- event_data.group_name
- event_data.resource_id
- event_data.resource_name
Validation & Testing
GET /grok-bot/setup-manifestsshows only reviewed manifests with no literal credentials- A dashboard review of every group’s Setup Scripts finds the same
- Every
grok_bot_team_setup_manifestevent, and everygrok_bot_group_resourceevent with resourcesetup_manifest, maps to an approved change - After a recreate (audited as
force_recreateorbulk_recreate), a pilot computer shows the EDR agent running
Expected result: Fleet-wide scripts are reviewed, carry no secrets, and install your own endpoint telemetry.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 2.7 | Allowlist Authorized Scripts |
| CIS Controls v8 | 4.1 | Establish and Maintain a Secure Configuration Process |
| CIS Controls v8 | 10.1 | Deploy and Maintain Anti-Malware Software |
| NIST 800-53 | CM-3 | Configuration Change Control |
| NIST 800-53 | CM-5 | Access Restrictions for Change |
| NIST 800-53 | SI-4 | System Monitoring |
| NIST 800-53 | IA-5(7) | No Embedded Unencrypted Static Authenticators |
| SOC 2 | CC8.1 | Change management |
| SOC 2 | CC6.1 | Logical access security |
| ISO 27001:2022 | A.8.9 | Configuration management |
| ISO 27001:2022 | A.8.32 | Change management |
| ISO 27001:2022 | A.5.17 | Authentication information |
| Product benchmark | — | No product benchmark exists yet |
4. Autonomy & Approvals
4.1 Enforce Auto-review with Team Ask-First Rules
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls v8 | 4.1 |
| NIST 800-53 | AC-3, AC-6, SI-4 |
| OWASP Agentic 2026 | ASI01, ASI09 |
| OWASP LLM Top 10 (2025) | LLM06:2025 |
Description
Turn on Enforce Auto-review, which is Enterprise only and off by default, then add team Ask first rules. Check every group, because a group can lift the lock for its own members.
What Auto Review does: Auto Review is “an independent review model”. It evaluates shell commands, plugin calls, computer use, automation writes (changes to routines and event triggers) and delegation before they run, and it can let an action proceed, require approval or deny it. Without enforcement, members can turn it off.
Team Bots: enforcement “includes Team Bots in chats where nobody can answer an approval, such as teammates’ chats and Slack, which otherwise run without Auto-review” (Cursor: Grok Bot for teams).
How team rules combine: team rules apply on top of members’ rules, Ask first wins on conflict, and team rules stop applying if enforcement is turned off.
Group opt-out: a group can lift the lock for its own members with Don’t enforce for this group. GET /grok-bot/auto-review can read enforced as true while a group has lifted the lock, so the group check is part of this control.
Rationale
Why This Matters:
- This is the only admin-enforceable approval gate for actions inside the hosted computer; the Execution on Local Computer ceiling (2.4) governs the member’s own machine
- It is model-based and “doesn’t review every side effect. Memory writes and most settings changes are examples” (Cursor: Grok Bot security), so it complements 2.1, 2.4 and 3.1 rather than replacing them
- Without enforcement, Team Bots in Slack and teammates’ chats run without Auto-review, because nobody is there to approve
Attack Prevented: Unattended high-impact actions (sends, payments, deletions, production changes) after goal hijack (OWASP Agentic ASI01 Agent Goal Hijack, ASI09 Human-Agent Trust Exploitation; OWASP LLM06:2025 Excessive Agency)
Real-World Incidents:
-
Grok–Bankrbot “Morse code” transfer (NeuralTrust analysis by Alessandro Pignati, 2026-05-08), an analogy only: the attacker first sent a “Bankr Club Membership NFT” to Grok’s associated wallet to expand Grok’s permissions in the Bankr ecosystem. They then had Grok translate a Morse-code message, and the decoded text instructed Bankrbot to transfer 3 billion DRB tokens (about $150,000) to an attacker-controlled wallet. The system lacked a “human-in-the-loop” mechanism for a payment action.
This involved xAI’s Grok chatbot, not Grok Bot, and predates Grok Bot’s 2026-08-11 launch. It illustrates the excessive-agency and prompt-injection pattern that enforced Auto-review with Ask-first payment rules is meant to stop (NeuralTrust)
-
Brave research PoC on Perplexity Comet (2025-08-20): in a proof-of-concept demonstration, instructions hidden in a Reddit comment made an agentic browser extract the user’s email, request a Perplexity OTP, read it in the user’s already-signed-in mail, and post both back. Every Bot on a member’s computer shares that same signed-in-session primitive (Brave)
Prerequisites
- Cursor Enterprise. Self-serve Teams has no equivalent (see 4.2 and 5.2 for what Teams can do)
ClickOps Implementation
Step 1: Turn on enforcement
- Navigate to: Cursor dashboard (https://cursor.com/dashboard/bot) → Grok Bot page → Enforce Auto-review → On
Step 2: Add team rules
- On the same page, select Configure Rules
- Add Ask first rules. The vendor’s examples are production deployments, external email, payments and accepting legal terms
- Keep Allow automatically rules narrow. Changes save automatically
Step 3: Check every group
- Members > Groups → each team-owned group → Grok Bot tab → Agent Capabilities
- Confirm Don’t enforce for this group is not set, because it lifts the team lock for that group’s members
- Review group Auto-review rules too. Group rules “combine with the team’s Auto-review rules, with ‘Ask first’ winning when rules conflict” (Cursor: Grok Bot for teams). A group Allow automatically rule can plausibly widen auto-approval, since group settings only widen, but the exact effect is undocumented, so review every group rule (4.3)
Doc conflict: member labels. Confirm live.
- Cursor: Grok Bot settings places the Auto-review switch and rules in the General > Bot section
- Cursor’s teams and security pages and docs.x.ai say Settings → General → Auto-review, where members see team rules as locked rows
- The changelog’s V0.68.0 renames saved rules “Custom Rules”
Code Implementation
- Read:
GET /grok-bot/auto-reviewreturnsenforcedplusrules.allowandrules.block, and is documented as readable on every plan. It returns the team policy only, not a group’s Don’t enforce - Write:
PUT /grok-bot/auto-reviewreplaces the policy, with up to 20 instructions of 1,000 characters each per list, trimmed and deduped. It returns 403 when Enforce Auto-Review is not available to the team
What the pack does:
- Verify GETs the policy and reports a finding when
enforcedis nottrue, or whenrules.allowandrules.blockare both empty, because then no team instructions feed Auto Review. Exit 0 means no finding the API can see, not a compliant tenant: group opt-outs are invisible to it, so Validation step 2 is still required - Enforce GETs, then PUTs
enforcedset totruewhile re-sending the stored allow and block lists. That re-send is a precaution, not a documented requirement - Why it re-sends: the doc’s “Lock Enforce Auto-Review without changing instructions” example sends empty lists, but it says empty lists keep stored instructions only “if your team can’t set Auto-Review rules”. For teams that can, the effect of empty lists is undocumented
- Rule authoring stays in the dashboard. The UI’s Ask first and Allow automatically map to the API’s
blockandallowlists only by inference, until a live run confirms the mapping - Detection: the Sigma rule watches the OpenTelemetry stream for
cursor.grok_bot.message_deliverywithdestination_typeemailandresultsent, which is mail from one of the member’s Bot addresses - What the rule skips: drafted emails awaiting approval are destination type
draft, and sends through a connected Gmail or Outlook plugin aremcp_tool_callrows the rule does not select - Other signals: the stream also carries
cursor.grok_bot.tool_decision(decision.sourcehuman,policy,hookorautomatic) andcursor.grok_bot.guardrail(guardrail.kindtool_escalation) - Audit: no
setting_namefor the enforce toggle is documented.team_settingsmay carry it, which needs live validation, andgrok_bot_group_settingsrecords a group’s Don’t enforce change
Code Pack: API Script
verify() {
read_auto_review
echo "Grok Bot 4.1 — enforced=$(jq -r '.enforced' "${GB_BODY_FILE}")" \
"allow=$(jq '.rules.allow | length' "${GB_BODY_FILE}") block=$(jq '.rules.block | length' "${GB_BODY_FILE}")"
jq -r '(.rules.block[] | " block: \(.)"), (.rules.allow[] | " allow: \(.)")' "${GB_BODY_FILE}"
if [ "$(jq -r '.enforced' "${GB_BODY_FILE}")" != "true" ]; then
gb_finding "Enforce Auto-Review is off: members can switch Auto-review off, and Team Bots in Slack and teammates' chats run without it (TRAP 4)"
fi
if [ "$(jq '(.rules.allow | length) + (.rules.block | length)' "${GB_BODY_FILE}")" -eq 0 ]; then
gb_finding "no team Auto-review rules: rules.allow and rules.block are both empty, so no team instructions feed Auto Review. Add 'Ask first' rules with Configure Rules on the Grok Bot page (TRAP 5)"
fi
echo " (team policy only — a group can set \"Don't enforce for this group\"; see the 4.3 pack)"
GB_UNPROVEN="group opt-outs ('Don't enforce for this group' is not readable by any API — check each group's Grok Bot tab, see 4.3)"
}
enforce() {
local body
# TRAP 1: re-read right before the PUT and send the stored lists back unchanged.
read_auto_review
if [ "$(jq -r '.enforced' "${GB_BODY_FILE}")" = "true" ]; then
echo " enforced is already true; team rules are authored in the dashboard (Configure Rules, TRAP 3), so there is nothing to send"
return 0
fi
body=$(jq -c '{enforced: true, rules: {allow: .rules.allow, block: .rules.block}}' "${GB_BODY_FILE}")
if [ "${GB_APPLY}" -ne 1 ]; then
gb_plan PUT /grok-bot/auto-review "${body}"
return 0
fi
gb_put team /grok-bot/auto-review "${body}"
if [ "${GB_CODE}" = "403" ]; then
echo "PRECONDITION: PUT /grok-bot/auto-review returned 403 — Enforce Auto-Review is not available to this team, or the team cannot set Auto-Review rules and the stored lists are non-empty (TRAP 2)" >&2
exit 2
fi
gb_require_2xx "PUT /grok-bot/auto-review"
# The response echoes the policy: the instructions must come back as sent.
if [ "$(jq -S -c '.rules' "${GB_BODY_FILE}")" != "$(jq -S -c '.rules' <<<"${body}")" ]; then
gb_fail "the PUT changed the stored instructions — compare the dashboard's rules with: $(jq -c '.rules' "${GB_BODY_FILE}")"
fi
echo " PUT /grok-bot/auto-review -> enforced=$(jq -r '.enforced' "${GB_BODY_FILE}"), instructions unchanged"
}
Code Pack: Sigma Detection Rule
# destination_type 'email' is mail from the user's Bot addresses; drafts
# awaiting approval are 'draft', and sends through a connected Gmail/Outlook
# plugin are mcp_tool_call rows; neither is selected.
detection:
selection_event:
- event_name: 'cursor.grok_bot.message_delivery'
- body: 'grok_bot_message_delivery'
selection_email_sent:
cursor.grok_bot.message_delivery.destination_type: 'email'
cursor.grok_bot.message_delivery.result: 'sent'
condition: selection_event and selection_email_sent
fields:
- cursor.user.email
- cursor.conversation.id
- cursor.event.id
- cursor.grok_bot.tool_call.id
- cursor.grok_bot.turn.id
- cursor.grok_bot.initiated_by
- cursor.grok_bot.provenance
Validation & Testing
GET /grok-bot/auto-reviewreturns"enforced": truewith the expected rules- No group’s Grok Bot tab has Don’t enforce for this group set
- For a member outside such groups, Settings → General → Auto-review shows the team rules locked and cannot be switched off, and a test request to email an outside address produces an approval card
- Ask a Team Bot in a Slack test thread to take an Ask-first action. Expect it not to run, with a
tool_decisionrecord (Action Recording on) showingdecision.sourcepolicyand no card. Do not assert outcomeheld, which means a card was withdrawn before anyone answered
Expected result: Members cannot switch Auto-review off, and Ask-first actions never run unattended.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 4.1 | Establish and Maintain a Secure Configuration Process |
| NIST 800-53 | AC-3 | Access Enforcement |
| NIST 800-53 | AC-6 | Least Privilege |
| NIST 800-53 | SI-4 | System Monitoring |
| SOC 2 | CC6.1 | Logical access security |
| SOC 2 | CC8.1 | Change management |
| ISO 27001:2022 | A.5.15 | Access control |
| ISO 27001:2022 | A.8.9 | Configuration management |
| OWASP Agentic 2026 | ASI01 | Agent Goal Hijack |
| OWASP Agentic 2026 | ASI09 | Human-Agent Trust Exploitation |
| OWASP LLM Top 10 (2025) | LLM06:2025 | Excessive Agency |
| Product benchmark | — | No product benchmark exists yet |
4.2 Set Required Team Rules for Grok Bot
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls v8 | 4.1 |
| NIST 800-53 | PL-4, AC-6 |
Description
Add a short set of required Team Rules for Grok Bot that cover data handling, external posting and personal-account boundaries. The control is available on Teams and Enterprise, and Team Rules apply to the whole team on both.
How rules work: Team Rules are guidance every member’s Bots follow, and “Rules applied to Grok Bot are always required, so members cannot turn them off.” Keep them short and few; the vendor’s example is “never move company data to personal accounts”.
Limits:
- “Always required” stops members, not admins: an admin or an API key can PATCH a rule to
enabledfalse - Rules guide a Bot and are not an approval gate. On Enterprise, pair them with Enforce Auto-review (4.1); on self-serve Teams that pairing is not available
Rationale
Why This Matters:
- On self-serve Teams, where Enforce Auto-review and Network Controls do not exist, Team Rules are the only org-wide instruction-level policy over Bot behavior. The capability switches in 2.3, 2.4 and 5.1 and the connector policy (2.1) also apply on Teams
- Team Bots in teammates’ chats, Slack and group chats act without anyone to approve, and they “follow the same team controls as every Bot, including” Team Rules (Team Bots)
- A short, required rule set gives every Bot the same data-handling boundary regardless of which member runs it
Attack Prevented: Well-intentioned Bots moving company data to personal accounts or posting it where it should not go
ClickOps Implementation
Step 1: Add the rules
- Navigate to: Cursor dashboard → Grok Bot page (https://cursor.com/dashboard/bot)
- Add each rule and scope it to Grok Bot, or to both Cursor and Grok Bot. The vendor’s text is “Add them from the Grok Bot page and scope each rule to Cursor, Grok Bot, or both.”
- The in-UI section and button labels for Team Rules are not documented, so transcribe them live. General Cursor Team Rules have a separate dashboard page (https://cursor.com/dashboard/team-content)
Code Implementation
Routes:
GET /grok-bot/team-ruleslists rules newest first (id,name,content,enabled,scope), paged at 50 by default and 100 at most, with cursor pagingPOST /grok-bot/team-rulesrequiresname(1 to 255 characters),content(1 to 30,000 characters) andenabled, and returns 201; a team can store up to 50 Grok Bot rulesPATCH /grok-bot/team-rules/:idrequires at least one field and returns 404 when the rule is missing, andDELETEreturns 204
Scope: there is no scope parameter. API-created rules come back with scope grokBot, so the API cannot create a rule scoped to both Cursor and Grok Bot. The doc does not show how a rule scoped to both appears in the list, so the verify region never drops rules that lack scope grokBot.
What the pack does:
- Verify diffs the list against a reviewed rule file and flags
enabledfalse - Enforce creates missing reviewed rules and re-enables reviewed rules found disabled, but only when their content still matches the reviewed file
- Drift: a disabled rule whose content drifted stays disabled and is reported, because Grok Bot rules are always required and re-enabling it would make unreviewed text binding on every member. Content drift and unexpected rules are reported, never overwritten or deleted
- Plan reach: writes are confirmed for Enterprise; Teams-plan reach is unconfirmed (8.6)
- Detection: the Sigma rule alerts on
team_rulewith actiondelete, or actionupdatewithis_activefalse - Detection caveat:
team_ruleis the generic Cursor team-rule event, listed under Team settings and integrations rather than in the Grok Bot table, and its payload has no scope field. Its coverage of Grok-Bot-scoped rules is therefore inferred, not documented - Group Rules: they log separately as
grok_bot_group_resourcewith resourcerule
Code Pack: API Script
verify() {
local want report
LIVE_RULES=$(list_rules)
echo "Grok Bot 4.2 — team rules: $(jq 'length' <<<"${LIVE_RULES}")"
jq -r '.[] | " - [\(if .enabled then "on " else "OFF" end)] \(.name) (scope=\(.scope // "?"))"' <<<"${LIVE_RULES}"
if [ "$(jq 'length' <<<"${LIVE_RULES}")" -eq 0 ]; then
gb_finding "no Grok Bot team rules exist"
fi
if [ "$(jq '[.[] | select(.enabled == false)] | length' <<<"${LIVE_RULES}")" -gt 0 ]; then
gb_finding "disabled team rules: $(jq -r '[.[] | select(.enabled == false) | .name] | join("; ")' <<<"${LIVE_RULES}")"
fi
if [ "$(jq '[.[].name] | length != (unique | length)' <<<"${LIVE_RULES}")" = "true" ]; then
gb_finding "two or more live rules share a name — the reviewed-file diff below is ambiguous"
fi
if [ -n "${HTH_RULES_FILE:-}" ]; then
want=$(reviewed_rules)
report=$(jq -r --argjson want "${want}" '
. as $live
| ($want[] | . as $w | ($live | map(select(.name == $w.name)) | first) as $l
| if $l == null then "MISSING: \($w.name)"
elif $l.content != $w.content then "DRIFT: \($w.name) (content differs from the reviewed file)"
else empty end),
($live[] | select(.name as $n | ($want | map(.name) | index($n)) == null) | "UNEXPECTED: \(.name)")' <<<"${LIVE_RULES}")
if [ -n "${report}" ]; then
sed 's/^/ /' <<<"${report}"
gb_finding "team rules differ from ${HTH_RULES_FILE}"
fi
fi
}
enforce() {
local want missing disabled held total body id name
want=$(reviewed_rules)
missing=$(jq -c --argjson live "${LIVE_RULES}" '[.[] | select(.name as $n | ($live | map(.name) | index($n)) == null)]' <<<"${want}")
# Re-enable only a disabled rule whose name AND content match a reviewed rule.
disabled=$(jq -c --argjson want "${want}" '[.[] | . as $l | select(.enabled == false and any($want[]; .name == $l.name and .content == $l.content))]' <<<"${LIVE_RULES}")
held=$(jq -r --argjson want "${want}" '.[] | . as $l | select(.enabled == false and any($want[]; .name == $l.name and .content != $l.content)) | .name' <<<"${LIVE_RULES}")
while IFS= read -r name; do
[ -n "${name}" ] || continue
echo " not re-enabled (content differs from the reviewed file — review first): ${name}"
done <<<"${held}"
total=$(( $(jq 'length' <<<"${LIVE_RULES}") + $(jq 'length' <<<"${missing}") ))
if [ "${total}" -gt 50 ]; then
echo "PRECONDITION: creating $(jq 'length' <<<"${missing}") rule(s) would exceed the team's 50-rule limit" >&2
exit 2
fi
while IFS= read -r body; do
[ -n "${body}" ] || continue
if [ "${GB_APPLY}" -ne 1 ]; then gb_plan POST /grok-bot/team-rules "${body}"; continue; fi
gb_post team /grok-bot/team-rules "${body}"
gb_require "POST /grok-bot/team-rules" 201
echo " created: $(jq -r '.teamRule.name' "${GB_BODY_FILE}") ($(jq -r '.teamRule.id' "${GB_BODY_FILE}"))"
done < <(jq -c '.[] | {name, content, enabled: true}' <<<"${missing}")
while IFS=$'\t' read -r id name; do
[ -n "${id}" ] || continue
if [ "${GB_APPLY}" -ne 1 ]; then gb_plan PATCH "/grok-bot/team-rules/${id}" '{"enabled": true}'; continue; fi
gb_patch team "/grok-bot/team-rules/$(jq -rn --arg i "${id}" '$i|@uri')" '{"enabled": true}'
gb_require_2xx "PATCH /grok-bot/team-rules/${id}"
echo " re-enabled: ${name}"
done < <(jq -r '.[] | [.id, .name] | @tsv' <<<"${disabled}")
}
Code Pack: Sigma Detection Rule
# Admin API pull shape (event_type + event_data.<field>). team_rule has no scope
# field, so Cursor editor rules match too; Grok Bot coverage is inferred.
detection:
selection_deleted:
event_type: 'team_rule'
event_data.action: 'delete'
selection_disabled:
event_type: 'team_rule'
event_data.action: 'update'
event_data.is_active:
- false
- 'false'
condition: 1 of selection_*
fields:
- timestamp
- user_email
- ip_address
- application_type
- event_data.action
- event_data.rule_id
- event_data.rule_name
- event_data.is_active
- event_data.is_required
Validation & Testing
GET /grok-bot/team-ruleslists the approved rules withenabledtrue, and the verify pack reports no disabled or unexpected rules- A member cannot disable the rules in the app
Expected result: Every member’s Bots carry the same required data-handling rules.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 4.1 | Establish and Maintain a Secure Configuration Process |
| NIST 800-53 | PL-4 | Rules of Behavior |
| NIST 800-53 | AC-6 | Least Privilege |
| SOC 2 | CC2.2 | Internal communication of information security responsibilities |
| SOC 2 | CC6.1 | Logical access security |
| ISO 27001:2022 | A.5.10 | Acceptable use of information and other associated assets |
| Product benchmark | — | No product benchmark exists yet |
4.3 Audit Group Grok Bot Tabs for Widening Overrides
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls v8 | 4.1, 8.11 |
| NIST 800-53 | CM-3, CM-6, AU-6 |
Description
Keep every group’s Grok Bot tab at the team value unless an exception is approved and recorded, and review the tabs on a schedule and on every group-settings audit event. Enterprise only.
How group settings combine: “Group settings only widen: a group can grant its members more than the team allows, never less, and a member in several groups gets the most permissive result.”
What a group tab can change:
- re-allow Cloud Agents
- raise the local-execution ceiling
- re-enable local egress
- lift Enforce Auto-review with Don’t enforce for this group
- add group Auto-review rules, which combine with the team’s, with Ask first winning on conflict. A group Allow automatically rule can plausibly widen auto-approval, but the exact effect is undocumented, so review every group rule
Network works differently: a group’s network policy “replaces the team’s for their members” unless the team policy is locked (Lock for All Groups), and the docs do not say how several group network policies combine.
Where to tighten: tighten controls on the Grok Bot page (2.3, 2.4, 3.1, 3.2, 4.1) and lock the network policy (3.1).
Rationale
Why This Matters:
- One group override undoes team-wide hardening for its members, and it is invisible on the team-level Grok Bot page; on Enterprise it is audit-logged
- For SCIM-synced directory groups, membership is managed in the IdP and Cursor shows it read-only, so who a group’s overrides reach is decided outside Cursor
- No API reads a group tab’s current state, so only a dashboard review proves it
Attack Prevented: Control erosion through permissive group exceptions
ClickOps Implementation
Step 1: Review each group tab
- Navigate to: Cursor dashboard → Members > Groups (https://cursor.com/dashboard/members?subtab=groups) → [a group your team owns] → Grok Bot tab
- Agent Capabilities: Cloud Agents, the Execution on Local Computer ceiling, Allow Local Egress, Auto-review’s Don’t enforce for this group, and group Auto-review rules
- Network: the group’s own policy, labelled Group Network Access
- Group Rules and Setup Scripts
- Reset anything that differs from the team value unless a recorded exception covers it
Step 2: Lock the network policy
- On the Grok Bot page’s Network section, turn on Lock for All Groups (3.1)
Doc conflict: tab label. The SCIM doc names the members tab Members & Groups with a Directory Groups subtab (SCIM), while the Grok Bot doc says Members > Groups. Confirm the label live.
Code Implementation
Automation: ClickOps only — Cursor (the Grok Bot admin plane) exposes no write interface for this setting (Admin API, Grok Bot, 2026-10-08). The pack below is read-only verification.
- No group route: no route reads or writes a group’s Grok Bot tab. All 17 Grok Bot routes are team-level, the team directory-group routes carry no Grok Bot fields, and the Organization API has only computer operations
- The one neutralizing write:
PUT /grok-bot/networkwithlockedset totrue(3.1) neutralizes a group override, and it covers network only
What the pack reads:
- Audit events from
GET /teams/audit-logs. The API defaults to 7 days and allows at most 30 days per request and 500 events per page, and retention is undocumented. The pack looks back 30 days by default (HTH_LOOKBACK_DAYS) and walks consecutive 30-day windows ofgrok_bot_group_settingsandgrok_bot_group_resource - The network lock: it also reports
lockedfromGET /grok-bot/network, which is readable on every plan - Organization scope: with
HTH_AUDIT_SCOPE=orgit requiresCURSOR_TEAM_ID, so events from other linked teams are not mixed into the one team whose lock it checks
What it cannot see: the pack detects changes, not current state. An override set before the window cannot be seen through any API, so exit 0 is not a compliant tenant and the dashboard review in Validation step 1 is still required.
setting_namevalues are not enumerated, so whether group network-policy and group Auto-review-rule edits land ingrok_bot_group_settingsis undocumented- Payloads never carry Group Rule or Setup Script text
Detection: a Sigma rule alerts on both event types.
Code Pack: API Script
verify() {
local days events
echo "Grok Bot 4.3 — group Grok Bot tab overrides"
# State: the one group override an API can neutralize is network policy.
gb_get team /grok-bot/network
gb_require "GET /grok-bot/network" 200
gb_shape '.locked | type == "boolean"' "GET /grok-bot/network"
echo " team network policy locked=$(jq -r '.locked' "${GB_BODY_FILE}")"
if [ "$(jq -r '.locked' "${GB_BODY_FILE}")" != "true" ]; then
gb_finding "network policy is not locked: a group's own network policy replaces the team's for its members (fix with the 3.1 pack)"
fi
# Change: every group setting, Group Rule and Group Setup Script edit (TRAP 2, TRAP 3).
days=$(gb_lookback_days 30)
events="$(gb_tmp events)"
gb_audit_pull "grok_bot_group_settings,grok_bot_group_resource" "${days}" "${events}"
echo " group changes in the last ${days} day(s): $(wc -l < "${events}" | tr -d ' ')"
jq -r '" \(.timestamp) by \(.user_email // "unknown") group=\(.event_data.group_name // .event_data.group_id // "?") " +
(if .event_type == "grok_bot_group_settings" then
"setting \(.event_data.setting_name // "?"): \(.event_data.old_value | tojson) -> \(.event_data.new_value | tojson)"
else
"\(.event_data.resource // "?") \(.event_data.action // "?"): \(.event_data.resource_name // .event_data.resource_id // "?")"
end)' "${events}"
if [ -s "${events}" ]; then
gb_finding "$(wc -l < "${events}" | tr -d ' ') group Grok Bot tab change(s) — each needs a recorded exception, or a revert in the dashboard"
fi
echo " NOT VERIFIABLE BY API: overrides set before the window (TRAP 1) — review every group's Grok Bot tab"
GB_UNPROVEN="current group Grok Bot tab state (overrides set before the ${days}-day window) — review every group's tab in the dashboard"
}
Code Pack: Sigma Detection Rule
# Admin API pull shape (event_type + event_data.<field>). setting_name values
# are not enumerated, so every group change is selected.
detection:
selection_group_setting:
event_type: 'grok_bot_group_settings'
selection_group_resource:
event_type: 'grok_bot_group_resource'
condition: 1 of selection_*
fields:
- timestamp
- user_email
- ip_address
- event_type
- event_data.group_id
- event_data.group_name
- event_data.setting_name
- event_data.old_value
- event_data.new_value
- event_data.resource
- event_data.action
- event_data.resource_id
- event_data.resource_name
Validation & Testing
- Only a dashboard review proves current state: every group’s Grok Bot tab matches the team values or has a recorded exception
GET /grok-bot/networkreturns"locked": true- The read pack shows no unexplained
grok_bot_group_settingsorgrok_bot_group_resourceevents in the window
Expected result: No group widens a team control without a recorded exception.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 4.1 | Establish and Maintain a Secure Configuration Process |
| CIS Controls v8 | 8.11 | Conduct Audit Log Reviews |
| NIST 800-53 | CM-3 | Configuration Change Control |
| NIST 800-53 | CM-6 | Configuration Settings |
| NIST 800-53 | AU-6 | Audit Record Review, Analysis, and Reporting |
| SOC 2 | CC7.2 | Monitoring of system components for anomalies |
| SOC 2 | CC8.1 | Change management |
| ISO 27001:2022 | A.8.9 | Configuration management |
| Product benchmark | — | No product benchmark exists yet |
5. Sharing & Team Bots
5.1 Keep Public Template Sharing Off
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls v8 | 3.3 |
| NIST 800-53 | AC-21, AC-3 |
Description
Turn the team’s public template sharing policy off, so sharing stays team-only. The dashboard setting is available on Teams and Enterprise.
What a template carries: a Bot template carries the Bot’s identity, description, skills and routines, and “A public link can be opened by anyone who has it” (Create and manage Bots).
Defaults:
- Team policy: “Enterprise teams start with public sharing off; other teams start with it allowed” (Cursor: public template sharing)
- Per-link default: a separate setting. “Accounts on an Enterprise plan default to Team-only; other accounts default to a public link” (Create and manage Bots)
Enforcement: “Cursor enforces the policy on its servers, including for templates that are already public.” Group Grok Bot tabs cannot widen this setting, because their Agent Capabilities list omits it, so the team value is authoritative.
Vendor guidance: the vendor’s recommended configuration says “Keep public template sharing off unless members should publish Bot templates outside the team”.
Rationale
Why This Matters:
- Skills and routines encode internal processes and system names, and they can embed credentials
- Non-Enterprise teams start with public links allowed
- Even within the team, members should “Remove API keys, internal URLs, customer data, and anything else you would not put in a public document” before sharing
Attack Prevented: Leakage of internal workflows and embedded secrets through public template links
ClickOps Implementation
Step 1: Turn public sharing off
- Navigate to: Cursor dashboard → Grok Bot page (https://cursor.com/dashboard/bot)
- Set public template sharing to Off. The doc groups the setting under its Agent capabilities heading and gives no bolded in-UI label, so confirm the exact label live
Code Implementation
- Verify: the verify region reads
templateSharingfromGET /grok-bot/capabilities(all,team_only,noneornull). It treatsnullas non-compliant, becausenullrestores the team default, which is allowed on non-Enterprise teams - Enforce: the enforce region sends
PATCH /grok-bot/capabilitieswith{"templateSharing": "team_only"}, which matches the doc’s “Off keeps sharing team-only” - The value
none: it is listed but never defined, so verify exits 2 (not judged) on it until a live test shows it blocks public links, and enforce never overwrites it - Plan reach: writes are confirmed for Enterprise; no page says
/capabilitiesworks on Teams, so verify live on a Teams tenant (8.6) - Detection: the Sigma rule alerts on
grok_bot_resourcewithresource_typetemplateandvisibilityPUBLIC. Streamed audit events nest these fields under a key named after the event type, while Admin API pulls useevent_data - Blind spots: payloads never include template bodies, so a SIEM cannot see secrets inside templates, and no
setting_namefor the sharing policy itself is documented
Undocumented values: the Admin API does not define
none, and does not say which value the dashboard’s Off maps to. The pack enforcesteam_onlyand reportsnoneas unjudged.
Code Pack: API Script
verify() {
gb_read_capabilities
local caps="${GB_TMP_DIR}/capabilities.json" value
if ! jq -e 'has("templateSharing") and ((.templateSharing == null) or (.templateSharing | IN("all","team_only","none")))' "${caps}" >/dev/null; then
echo "PRECONDITION: GET /grok-bot/capabilities has no documented templateSharing value — nothing was judged" >&2
exit 2
fi
value=$(jq -r '.templateSharing // "null"' "${caps}")
echo "Grok Bot 5.1 — templateSharing: ${value}"
case "${value}" in
team_only) ;;
none)
echo "PRECONDITION: templateSharing=none is an undocumented value — confirm live that it blocks public template links before accepting it; not judged, and not overwritten (TRAP 2)" >&2
exit 2 ;;
all) gb_finding "members can publish Bot templates outside the team (templateSharing=all)" ;;
null) gb_finding "templateSharing=null follows the team default, which is 'allowed' on non-Enterprise teams — set team_only explicitly" ;;
esac
}
enforce() {
local body='{"templateSharing": "team_only"}'
if [ "${GB_APPLY}" -ne 1 ]; then
gb_plan PATCH /grok-bot/capabilities "${body}"
return 0
fi
gb_patch team /grok-bot/capabilities "${body}"
gb_require_2xx "PATCH /grok-bot/capabilities"
echo " PATCH /grok-bot/capabilities -> templateSharing=$(jq -r '.templateSharing // "null"' "${GB_BODY_FILE}")"
}
Code Pack: Sigma Detection Rule
# Admin API pull shape (event_type + event_data.<field>). visibility values are
# documented in upper case: TEAM, PUBLIC.
detection:
selection:
event_type: 'grok_bot_resource'
event_data.resource_type: 'template'
event_data.visibility: 'PUBLIC'
condition: selection
fields:
- timestamp
- user_email
- ip_address
- event_data.action
- event_data.resource_id
- event_data.visibility
- event_data.previous_visibility
Validation & Testing
GET /grok-bot/capabilitiesreturnstemplateSharingteam_only(ornoneonce confirmed live to block public links), neverallornull- A previously public template link no longer opens for a non-member
- Enterprise: there are no
grok_bot_resourceevents with visibilityPUBLIC
Expected result: Bot templates can be shared only inside the team.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 3.3 | Configure Data Access Control Lists |
| NIST 800-53 | AC-21 | Information Sharing |
| NIST 800-53 | AC-3 | Access Enforcement |
| SOC 2 | CC6.1 | Logical access security |
| SOC 2 | C1.1 | Identification and maintenance of confidential information |
| ISO 27001:2022 | A.5.14 | Information transfer |
| Product benchmark | — | No product benchmark exists yet |
5.2 Govern Team Bots, Their Shared Credentials, and Their Slack Apps
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls v8 | 3.3, 6.8 |
| NIST 800-53 | AC-6, AC-21, IA-5 |
| OWASP Agentic 2026 | ASI01, ASI06, ASI07 |
Description
Govern which Team Bots exist, who reaches them and which credentials they carry. A Team Bot shares one member’s plugins, secrets, skills, files and team memory with every teammate and with Slack, and admins cannot change any of that.
Status: Team Bots are in public beta on Teams and Enterprise (xAI launch post, 2026-09-28); the docs pages carry no beta label, and Cursor’s help notes Team Bots may not yet be on for every team.
- What is shared: “The Bot can use every secret in any teammate’s conversation”, and a plugin configured with a key is “The Bot’s own credential, the same for everyone” (Team Bots). Slack channels, group DMs and threads use “One shared computer for that Bot”, and posts from Slack workflows and other apps are exempt from the account-linking requirement
- Bot-to-Bot traffic: a teammate’s published Team Bot can be added to a group chat, and a Bot can “message another Bot to hand off work”. Of stopping that, Cursor says “There is no switch for this”; the only lever is an instruction in the group’s or the Bot’s description, such as “Don’t message other Bots. Ask me first.” (Cursor help: group chats)
- Who can change a Team Bot: xAI says “Only the Bot’s owner and its managers change its plugins, secrets, skills, and files”; Cursor’s help says only the owner can change a Team Bot (8.6)
- What admins cannot do: change those settings, or call a Team Bot API. No documented admin control disables Team Bot creation (as of 2026-10-09)
- The admin levers: Manage Team Bots (default assignment and delete), Slack’s own app approval, the connector policy (2.1), Team Rules (4.2) and, on Enterprise, Enforce Auto-review (4.1)
Require owners to use “scoped, read-only service-account keys instead of personal credentials”, and to choose Remote HTTPS rather than a Command custom MCP server when secrets are involved.
Rationale
Why This Matters:
- A Team Bot turns one member’s credentials and memory into a team-wide, Slack-reachable agent, and its team memory “is read by every teammate’s conversation” (Team Bots)
- Auto Review “doesn’t review every side effect. Memory writes and most settings changes are examples” (Cursor: Grok Bot security), so a poisoned team memory is not caught at write time
- On self-serve Teams, Enforce Auto-review does not exist, so Team Bots in teammates’ chats and Slack always run without Auto-review where nobody can approve. On Enterprise, a group can also lift enforcement with Don’t enforce for this group
Attack Prevented: Indirect prompt injection from Slack content hijacking a shared Team Bot to exfiltrate shared data (OWASP Agentic ASI01 Agent Goal Hijack), poisoned team memory that every teammate’s conversation reads (ASI06 Memory & Context Poisoning), and instructions relayed to a Team Bot by other Bots or by Slack workflow and app posts that skip account linking (ASI07 Insecure Inter-Agent Communication). The levers here limit which Team Bots exist and who reaches them; nothing stops Bot-to-Bot messaging itself
Real-World Incidents:
- PromptArmor (August 2024): an injection posted in a public Slack channel made Slack AI exfiltrate data from private channels the attacker was not in. Team Bots answer channel threads and workflow posts on one shared computer (PromptArmor)
- Radware ZombieAgent (Zvika Babo, 2026-01-08): instructions written into ChatGPT’s memory persisted across new chats and kept exfiltrating later conversations. This is the risk class for Team Bot team memory, which Auto Review does not review (Radware; read in a real browser on 2026-10-08)
Prerequisites
- Cursor Teams or Enterprise with Team Bots turned on for the team
- Slack workspace app approval kept on (slack 3.1)
ClickOps Implementation
Step 1: Set default assignment for each Team Bot
- Navigate to: Cursor dashboard (https://cursor.com/dashboard/bot) → Grok Bot page → Team Bots → Manage Team Bots (Team Bots: Add Team Bots for members by default)
- For each Team Bot, choose All team, None or specific groups. Groups come from Members & Groups
- Assignment only controls whose sidebar the Bot is forced into; those members see “Required by your admin. You can’t turn this off.” None does not restrict access: after a Bot is published, any teammate can still add it from New → Team Bots, from search, or from a link (Cursor help: Team Bots). To remove an unapproved Bot, delete it (Step 2)
Step 2: Delete unapproved Team Bots
- In Manage Team Bots, choose the trash icon on the Bot’s row → type the Bot’s exact name → Delete (Team Bots: Delete a Team Bot as an admin)
- Deleting removes the Bot’s Slack app. “If the Slack app can’t be removed, a Slack workspace admin can remove it in Slack”
Step 3: Keep Slack’s gate on
- Keep workspace app approval on in Slack, so Team Bot setup stops at Awaiting admin approval until a Slack admin approves the Team Bot’s app
Step 4: Narrow who can create Team Bots (Enterprise)
- No documented admin control disables Team Bot creation (as of 2026-10-09). On Enterprise, Manage Group Access (1.1) limits it indirectly by limiting who has Grok Bot at all. Ask your Cursor account team whether Team Bots can be kept off for a team
Step 5: Keep each Team Bot’s managers to a minimum (owner action)
- In the Bot’s details → Setup → Managers, the owner removes anyone who no longer needs to change the Bot: trash icon (Remove manager) → Remove
- A manager “can change the Bot’s setup, like its profile, skills, plugins, secrets, and Slack app”, while “Publishing, unpublishing, and deleting the Bot stay with” the owner (Team Bots: Add managers)
- As an HTH process recommendation, because no audit event records manager changes, have each owner attest their Managers list at every Team Bot review. No page documents an admin view of a Bot’s managers
Code Implementation
Automation: ClickOps only — Cursor (the Grok Bot admin plane) exposes no write interface for this setting (Admin API, Grok Bot, 2026-10-08). The Grok Bot section has no Team Bot route, no capabilities field toggles Team Bots, and Manage Team Bots and admin delete are dashboard-only. The pack below is read-only verification.
The pack is an inventory, Enterprise only because audit logs are an Enterprise feature.
- Events: from
GET /teams/audit-logs, it pullsgrok_bot_created(withsourcedirect,template,agent_sdkorsystem),grok_bot_lifecycle(includingpublished,unpublishedanddelete),grok_bot_skillandslack_account_link - Organization-wide variant:
GET /organizations/audit-logswith an Organization key holdingauditlogs:read - Detection: a Sigma rule alerts on
grok_bot_lifecyclewith actionpublished - Not audited: no documented audit event covers Manage Team Bots default assignment, adding or removing managers, Team Bot secrets, plugins or files, or the creation and removal of a Team Bot’s Slack app
- Plugin claim: the Grok Bot changelog V0.57.0 says audit logs now record plugins, but the event table has no Grok Bot plugin event (8.6)
Code Pack: API Script
verify() {
local days events published unapproved
days=$(gb_lookback_days 30)
events="$(gb_tmp events)"
gb_audit_pull "grok_bot_created,grok_bot_lifecycle,grok_bot_skill,slack_account_link" "${days}" "${events}"
echo "Grok Bot 5.2 — Team Bot activity in the last ${days} day(s)"
echo " Bots created:"
jq -r 'select(.event_type == "grok_bot_created")
| " \(.timestamp) \(.event_data.agent_id // "?") \"\(.event_data.name // "?")\" source=\(.event_data.source // "?") by \(.user_email // "unknown")"' "${events}"
echo " Publishing, visibility and deletion:"
jq -r 'select(.event_type == "grok_bot_lifecycle"
and (.event_data.action | IN("published","unpublished","visibility_changed","delete")))
| " \(.timestamp) \(.event_data.agent_id // "?") \(.event_data.action)\(if .event_data.new_visibility then " -> \(.event_data.new_visibility)" else "" end) by \(.user_email // "unknown")"' "${events}"
echo " Skill changes:"
jq -r 'select(.event_type == "grok_bot_skill")
| " \(.timestamp) \(.event_data.agent_id // "?") \(.event_data.action // "?") \(.event_data.skill_slug // "?") by \(.user_email // "unknown")"' "${events}"
echo " Slack account links:"
jq -r 'select(.event_type == "slack_account_link")
| " \(.timestamp) \(.user_email // "unknown") \(.event_data.action // "?") slack_team=\(.event_data.slack_team_id // "?") workspace_changed=\(.event_data.workspace_changed // "?")"' "${events}"
echo " NOT VERIFIABLE BY API: Manage Team Bots assignment, managers, Team Bot secrets, plugins, files and Slack apps (TRAP 2; see its V0.57.0 conflict)"
GB_UNPROVEN="Team Bots published before the ${days}-day window, Manage Team Bots assignment, managers, and Team Bot secrets, plugins, files and Slack apps (dashboard and Slack only, TRAP 1 and TRAP 2)"
# Every publish to the team must map to an approved Bot.
published=$(jq -r 'select(.event_type == "grok_bot_lifecycle" and .event_data.action == "published") | .event_data.agent_id // "?"' "${events}" | sort -u)
if [ -z "${published}" ]; then return 0; fi
if [ -n "${HTH_APPROVED_AGENT_IDS:-}" ]; then
unapproved=$(jq -rn --arg ok "${HTH_APPROVED_AGENT_IDS}" --arg got "${published}" \
'($ok | split(",") | map(gsub("^\\s+|\\s+$"; ""))) as $a | $got | split("\n")[] | select(. as $g | $a | index($g) | not)')
else
unapproved="${published}"
fi
if [ -n "${unapproved}" ]; then
gb_finding "Bots published to the team without a matching approval: $(echo "${unapproved}" | tr '\n' ' ')"
fi
}
Code Pack: Sigma Detection Rule
# Admin API pull shape (event_type + event_data.<field>). No documented audit
# event covers a Team Bot's managers, secrets, plugins, files or Slack app.
detection:
selection:
event_type: 'grok_bot_lifecycle'
event_data.action: 'published'
condition: selection
fields:
- timestamp
- user_email
- ip_address
- event_data.agent_id
- event_data.action
- event_data.changed_fields
- event_data.old_visibility
- event_data.new_visibility
Validation & Testing
- Manage Team Bots lists only approved Team Bots with the intended assignment. This half can only be checked in the dashboard
- Every
grok_bot_lifecyclepublishedevent maps to an approved request - Slack’s app list shows Team Bot apps only for approved Bots
Expected result: Every Team Bot is approved, assigned deliberately, and visible in Slack only through an approved app.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 3.3 | Configure Data Access Control Lists |
| CIS Controls v8 | 6.8 | Define and Maintain Role-Based Access Control |
| NIST 800-53 | AC-6 | Least Privilege |
| NIST 800-53 | AC-21 | Information Sharing |
| NIST 800-53 | IA-5 | Authenticator Management |
| SOC 2 | CC6.1 | Logical access security |
| SOC 2 | CC6.3 | Role-based access, modification and removal |
| ISO 27001:2022 | A.5.15 | Access control |
| ISO 27001:2022 | A.5.17 | Authentication information |
| OWASP Agentic 2026 | ASI01 | Agent Goal Hijack |
| OWASP Agentic 2026 | ASI06 | Memory & Context Poisoning |
| OWASP Agentic 2026 | ASI07 | Insecure Inter-Agent Communication |
| Product benchmark | — | No product benchmark exists yet |
6. Monitoring & Audit
6.1 Turn On Action Recording and Export It over OpenTelemetry
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls v8 | 8.2, 8.5, 8.9 |
| NIST 800-53 | AU-2, AU-3, AU-12, SI-4 |
| OWASP Agentic 2026 | ASI10 |
Description
Turn on Action Recording and OpenTelemetry Export, and route both to the SIEM. Action Recording is Enterprise only and off by default.
What it records: audit logs record control-plane changes, not what Bots did. Action Recording records sanitized metadata for each tool call and decision:
- MCP calls
- shell commands (secret-scrubbed, at most 8 KiB)
- browser navigations as
scheme://host/pathwith query strings, fragments and credentials stripped - computer-use counts, file transfers, messages sent and routine runs
- guardrail interventions, and subagent and Cloud Agent handoffs
Where events go: events are kept in an internal store with 90-day retention (Grok Bot security), and “Recorded events do not appear on the Audit Log page.” OpenTelemetry Export is “the customer path for Action Recording events”, tagged cursor.surface=grok_bot.
Endpoint: it must be reachable from the public internet. Cursor sends from a fixed set of source IPs you can allowlist, but “Use TLS and auth as the primary control” (OpenTelemetry Export).
Caveats: turning a toggle back on never backfills. Privacy Mode (Legacy) forces recording off and blocks Grok Bot entirely.
L3 option: conversation content export, only where content-level forensics are required.
- It covers prompt and response text (capped at 32 KiB per message) and hosted-connector MCP Tool I/O (capped at 8 KiB per side);
stdioservers and built-in tools report no payloads - Redaction is pattern-based, and “your team owns that residual risk”
- The
conversation_contentfamily covers Cloud Agents and Grok Bot together (“Cloud Agents and Grok Bot only”), and there is no Grok-Bot-only toggle. Turning it on also exports Cloud Agent conversation text, so weigh that in the data-minimization review
Rationale
Why This Matters:
- Grok Bot “does not ship a built-in customer-facing telemetry or EDR feed” (Team Setup can install your own; see 3.3), and “Dedicated data loss prevention hooks are not available”
- Bots act as the signed-in member, so a target app’s identity record is the member’s, except where team-managed connectors use team or service-account credentials
- By default, hosted-computer traffic leaves from shared static egress ranges, which the vendor says to “treat … as identifying Grok Bot traffic rather than your team alone” (Grok Bot security); the ranges come from your account team
- A source-IP check can therefore flag Grok Bot computer traffic, but it cannot say which customer, member or Bot sent it, or separate a Bot’s step from a member who took control of the computer to sign in
- With Allow Local Egress (3.2), destinations see the member’s desktop IP instead, and with a Team Setup networking client (3.3), your network’s addresses. Connector tokens stay on Cursor’s backend, and the docs give no source address for connector calls
- These events are the only record of agent behavior
Attack Prevented: Undetected agent misuse, and actions that cannot be attributed to a Bot rather than a person (OWASP Agentic ASI10 Rogue Agents)
Prerequisites
- Cursor Enterprise, not on Privacy Mode (Legacy)
- An OTLP/HTTP collector reachable from the public internet over HTTPS
ClickOps Implementation
Step 1: Turn on Action Recording
- Navigate to: Cursor dashboard → Grok Bot page (https://cursor.com/dashboard/bot) → Action Recording → On
Step 2: Create an OpenTelemetry destination
- Team Settings → OpenTelemetry Export → Create destination
- Enter the HTTPS base URL with no
/v1suffix (Cursor appends/v1/metricsand/v1/logs) and an auth header such asAuthorization: Bearer <token> - Test connection → Enable
- Keep the
grok_bot_agent_actionsfamily toggle on. It defaults on for a new destination but can be turned off (OpenTelemetry Export) - Keep the
skills_hooks_pluginsfamily on too: a Bot’s skill reads arrive ascursor.skill.activatedin that family, and they are Action Recording data
Step 3 (L3, optional): Conversation content
- Team Settings → OpenTelemetry Export → Allow conversation content export
- On the destination, turn on Conversation content. That turns on Prompts and Responses; Tool I/O stays off until you turn it on
Changelog vs docs: the changelog’s V0.30.0 says “Action Recording on the dashboard records members’ Grok Bot actions to your team’s audit log”. The current docs say “Recorded events do not appear on the Audit Log page” and route them through OpenTelemetry only. Under this repository’s fact-dispute rule the current docs win; confirm live.
Code Implementation
- Verify: the verify region reads
actionRecordingfromGET /grok-bot/capabilities(read:*oradmin:*) - Enforce: the enforce region sends
PATCH /grok-bot/capabilitieswith{"actionRecording": true}(Enterprise,admin:*, 403 when the field is not available) - Result line: with
actionRecordingtruethe pack’s result line reads “NOT proven: the OpenTelemetry destination” rather than “compliant”, because with no destination nothing leaves Cursor
Automation: the OpenTelemetry destination and conversation content export are ClickOps only — Cursor (the Grok Bot admin plane) exposes no write interface for them: the team Admin API, the Organization API and the API overview document no OpenTelemetry route (Admin API, Grok Bot, 2026-10-08).
Drift signal: the only API-side drift signal is the audit log, which the 6.2 pack pulls (Compliance and monitoring, 2026-10-09):
customer_telemetry_destination: a destination created, updated or deleted, withenabled,enabled_familiesanddisabled_familiescustomer_telemetry_content_opt_in: conversation-content export turned on or off
A destination deleted or misconfigured before the audit window is not visible that way.
On the wire, records arrive as OTLP/HTTP binary protobuf POSTed to <base>/v1/logs. The grok_bot_agent_actions family covers cursor.grok_bot.mcp_tool_call, shell_command, browser_navigation, computer_use_session, tool_result, tool_decision, file_transfer, message_delivery, routine_run, guardrail and delegation (Wire Reference).
Code Pack: API Script
verify() {
gb_read_capabilities
local caps="${GB_TMP_DIR}/capabilities.json"
if ! jq -e '.actionRecording | type == "boolean"' "${caps}" >/dev/null; then
echo "PRECONDITION: GET /grok-bot/capabilities has no boolean actionRecording — nothing was judged" >&2
exit 2
fi
echo "Grok Bot 6.1 — actionRecording=$(jq -r '.actionRecording' "${caps}")"
if [ "$(jq -r '.actionRecording' "${caps}")" != "true" ]; then
gb_finding "Action Recording is off: no record exists of the commands, browsing, file transfers or connector calls Bots make"
fi
echo " NOT VERIFIABLE BY API: the OpenTelemetry destination (Team Settings > OpenTelemetry Export)."
echo " Confirm a destination exists with the grok_bot_agent_actions family on, and that your"
echo " collector receives cursor.grok_bot.* log events with cursor.surface=grok_bot (TRAP 1, TRAP 3)."
echo " Drift signal: customer_telemetry_destination / customer_telemetry_content_opt_in audit rows (6.2 pack)."
GB_UNPROVEN="the OpenTelemetry destination and its grok_bot_agent_actions family (Team Settings only; watch customer_telemetry_* in the 6.2 pack)"
}
enforce() {
local body='{"actionRecording": true}'
if [ "${GB_APPLY}" -ne 1 ]; then
gb_plan PATCH /grok-bot/capabilities "${body}"
return 0
fi
gb_patch team /grok-bot/capabilities "${body}"
gb_require_2xx "PATCH /grok-bot/capabilities"
echo " PATCH /grok-bot/capabilities -> actionRecording=$(jq -r '.actionRecording' "${GB_BODY_FILE}")"
}
Validation & Testing
GET /grok-bot/capabilitiesreturns"actionRecording": true- The collector receives logs with
cursor.surface=grok_bot - A test shell command from a pilot Bot arrives as log event
cursor.grok_bot.shell_command. Route on the event name, since the body is the constantgrok_bot_shell_command
Expected result: Every Bot action reaches your SIEM as sanitized metadata.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 8.2 | Collect Audit Logs |
| CIS Controls v8 | 8.5 | Collect Detailed Audit Logs |
| CIS Controls v8 | 8.9 | Centralize Audit Logs |
| NIST 800-53 | AU-2 | Event Logging |
| NIST 800-53 | AU-3 | Content of Audit Records |
| NIST 800-53 | AU-12 | Audit Record Generation |
| NIST 800-53 | SI-4 | System Monitoring |
| SOC 2 | CC7.2 | Monitoring of system components for anomalies |
| SOC 2 | CC7.3 | Evaluation of security events |
| ISO 27001:2022 | A.8.15 | Logging |
| ISO 27001:2022 | A.8.16 | Monitoring activities |
| OWASP Agentic 2026 | ASI10 | Rogue Agents |
| Product benchmark | — | No product benchmark exists yet |
6.2 Monitor Grok Bot Control-Plane Audit Events
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls v8 | 8.2, 8.9, 8.11 |
| NIST 800-53 | AU-2, AU-6, SI-4 |
Description
Pull the Grok Bot audit events on a schedule or have them streamed, alert on the posture-changing types, and give routines their own alert. Select events by event type, never by application alone.
The event set:
- Grok Bot control-plane table: Enterprise audit logs carry
sand_onboarding,grok_bot_created,grok_bot_lifecycle,grok_bot_access_changed,grok_bot_team_setup_manifest,grok_bot_group_settings,grok_bot_group_resource,grok_bot_resource,grok_bot_skill,grok_bot_machine,grok_bot_vm,grok_bot_vm_bulkandgrok_bot_routine - Two more: the Grok Bot docs also count
mcp_authenticationandslack_account_linkas Grok Bot control-plane events - Telemetry events:
customer_telemetry_destinationandcustomer_telemetry_content_opt_inbelong in the same review, because they record someone blinding 6.1
Actors: actors appear as Bot: <owner email> when a Bot acts during its owner’s turn, and as System for Grok Bot events with no identified actor.
Why not filter by application: application_type is grok_bot only when the Grok Bot surface acted, and admin changes made in the dashboard or through the Admin API carry cursor, so select by event type, not by application_type alone.
Scope: cursor 10.1 covers generic audit export; this control is the Grok Bot event set.
Rationale
Why This Matters:
- Several posture changes have no preventive admin control or are made by members, so detection is the remaining control. They include widened access, group overrides, Team Setup edits, public templates, Team Bot publication and routine creation
- Routines run unattended on a schedule, or “when something happens, such as a Slack message, a GitHub, Linear, Sentry, or PagerDuty event, an email, or a webhook call”. Slack triggers can watch one channel or all of Slack, including “on any message”, and webhook senders pass
Authorization: Bearer <key>(Cursor help: Routines) - Every one of those triggers is untrusted input. There is no routine-specific admin control, and members create, edit, pause and delete them
grok_bot_routineis the only default-on, control-plane record of routine create, update, enable, disable and delete. Routines are edited in place, so anupdatecan repoint an existing routine at a broader trigger or a new instruction- The nearest preventive lever is Enforce Auto-review with team rules (4.1), since Auto Review covers automation writes; those approvals go to the member, not the admin
Attack Prevented: Unnoticed persistence through routines, and configuration drift
Prerequisites
- Cursor Enterprise and admin access (Compliance and monitoring)
ClickOps Implementation
Step 1: Review in the dashboard
- Navigate to: Cursor dashboard → Audit Log page (https://cursor.com/dashboard/audit-log)
- Filter by event type, using the Grok Bot event types listed in the Description plus
mcp_authentication,slack_account_linkand the twocustomer_telemetry_*types, and add a date range or actor if you need one. Do not filter by application alone:- Admin changes made on the Grok Bot page of the Cursor dashboard, on a group’s Grok Bot tab, or through the Admin API carry application
cursor, notgrok_bot(“cursorfor … cursor.com, and the Admin API”, Compliance and monitoring) - That includes enabling Grok Bot, Manage Group Access, Team Setup and group settings
- An application filter of Grok Bot is useful only as an extra view of actions the Grok Bot surface itself took
- Admin changes made on the Grok Bot page of the Cursor dashboard, on a group’s Grok Bot tab, or through the Admin API carry application
- Export the filtered result to CSV; the export includes an Application column
Step 2: Stream to the SIEM
- Set up streaming as in cursor 10.1; Cursor arranges it by contact (hi@cursor.com). Route the event types in Step 1 to the Grok Bot alerts below
Step 3: Add per-run routine visibility
- Per-run visibility needs Action Recording plus OpenTelemetry Export (6.1):
cursor.grok_bot.routine_runevents. Select routine-started turns on the always-present resource attributecursor.entrypoint=automation, withcursor.grok_bot.initiated_by=routineas a secondary check, since that attribute is optional and absent from older Grok Bot versions (Wire Reference)
Code Implementation
Automation: ClickOps only — Cursor (the Grok Bot admin plane) exposes no write interface for this setting (Compliance and monitoring, 2026-10-08). Audit logging has no documented enable or disable control and no streaming configuration endpoint, and streaming is arranged by contacting Cursor. The pack below is read-only verification.
The pack calls GET /teams/audit-logs (Admin API: audit logs) with a Team key, or GET /organizations/audit-logs with an Organization key holding auditlogs:read or admin:*.
- Parameters:
startTime,endTime,eventTypes,search,users,pageandpageSize. There is noapplication_typefilter parameter - Limits: the API defaults to 7 days and allows at most 30 per request, at 500 events per page and 20 requests per minute, oldest first
- Event set: the Grok Bot event types plus
mcp_authentication,slack_account_link,customer_telemetry_destination,customer_telemetry_content_opt_inandteam_settings, pulled in 30-day windows as JSONL. Every row is selected byevent_type; the pack countsapplication_typefor context and never uses it to select or drop rows - Flags: posture-changing rows, including routine
create,updateandenable(a routine is edited in place, so an update can repoint a benign routine at a broad trigger), everycustomer_telemetry_destinationchange (a delete orenabledfalseblinds 6.1) and every conversation-content opt-in change - Review block:
team_settingsrows whosesetting_nameis not one of the documented common names are listed separately, unflagged. Team-wide Grok Bot toggles may land there, because the event fires for changes “including changes made through the Admin API” and its name list is “not an exhaustive list”, but no Grok Botsetting_nameis documented - Routine rule: one Sigma rule alerts on
grok_bot_routinewith actioncreate,updateorenable;trigger_typevalues are not enumerated, so it does not filter on them - Telemetry rule: a second alerts on
customer_telemetry_destinationwith actiondeleteorenabledfalse, and oncustomer_telemetry_content_opt_inwithenabledtrue, all documented field values
Code Pack: API Script
verify() {
local days events flagged review count
days=$(gb_lookback_days 1)
events="$(gb_tmp events)"
gb_audit_pull "${GROK_BOT_EVENTS}" "${days}" "${events}"
count=$(wc -l < "${events}" | tr -d ' ')
echo "Grok Bot 6.2 — ${count} Grok Bot audit event(s) in the last ${days} day(s)"
jq -s -r 'group_by(.event_type) | map(" \(.[0].event_type): \(length)") | .[]' "${events}"
echo " by acting surface (context only — TRAP 1):"
jq -s -r 'group_by(.application_type // "") | map(" \(if .[0].application_type == "" or .[0].application_type == null then "(unknown)" else .[0].application_type end): \(length)") | .[]' "${events}"
# Posture-changing rows, selected by event_type and documented field values only.
flagged=$(jq -r '
(if .event_type == "sand_onboarding" then
"Grok Bot \(if .event_data.new_completed == true then "ENABLED" else "DISABLED" end) for the team"
elif .event_type == "grok_bot_access_changed" then
"access \(.event_data.old_mode // "?") -> \(.event_data.new_mode // "?")\(if .event_data.new_mode == "all" then " (WIDENED to every member)" else "" end)"
elif .event_type == "grok_bot_team_setup_manifest" then
"team setup manifest \(.event_data.action // "?"): \(.event_data.manifest_id // "?") (\(.event_data.entry_count // "?") entries)"
elif .event_type == "grok_bot_group_settings" then
"group \(.event_data.group_name // "?") setting \(.event_data.setting_name // "?") changed"
elif .event_type == "grok_bot_group_resource" then
"group \(.event_data.group_name // "?") \(.event_data.resource // "?") \(.event_data.action // "?")"
elif .event_type == "grok_bot_resource" and .event_data.visibility == "PUBLIC" then
"\(.event_data.resource_type // "?") \(.event_data.resource_id // "?") is PUBLIC (\(.event_data.action // "?"))"
elif .event_type == "grok_bot_lifecycle" and .event_data.action == "published" then
"Bot \(.event_data.agent_id // "?") published to the team"
elif .event_type == "grok_bot_routine" and (.event_data.action | IN("create","update","enable")) then
"routine \(.event_data.action) \"\(.event_data.name // "?")\" on Bot \(.event_data.sand_agent_id // "?") trigger=\(.event_data.trigger_type // "?")"
elif .event_type == "customer_telemetry_destination" then
"OpenTelemetry destination \(.event_data.action // "?") \(.event_data.destination_id // "?")"
+ (if .event_data.action == "delete" then " — 6.1 export path REMOVED" else "" end)
+ (if .event_data.enabled == false then " — destination DISABLED" else "" end)
+ " enabled_families=\(.event_data.enabled_families | tojson) disabled_families=\(.event_data.disabled_families | tojson)"
elif .event_type == "customer_telemetry_content_opt_in" then
(if .event_data.enabled == true then "conversation-content export turned ON (prompts, responses and tool I/O may now leave Cursor; L3 decision)"
else "conversation-content export turned OFF" end)
elif .event_type == "grok_bot_machine" and .event_data.action == "register" then
"local computer registered: \(.event_data.machine_id // "?")"
elif .event_type == "grok_bot_vm_bulk" then
"bulk computer operation \(.event_data.action // "?"): target=\(.event_data.target_count // "?") failed=\(.event_data.failed_count // "?")"
else empty end) as $why
| " \(.timestamp) by \(.user_email // "unknown") \($why)"' "${events}")
echo " posture-changing events (review each):"
if [ -n "${flagged}" ]; then echo "${flagged}"; else echo " none"; fi
# Unflagged review block: team_settings names the docs do not list (TRAP 3).
review=$(jq -r --argjson known "${DOCUMENTED_TEAM_SETTINGS}" '
select(.event_type == "team_settings" and ((.event_data.setting_name // "") as $n | $known | index($n) | not))
| " \(.timestamp) by \(.user_email // "unknown") \(.event_data.setting_name // "?"): \(.event_data.old_value | tojson) -> \(.event_data.new_value | tojson)"' "${events}")
echo " undocumented team_settings changes (may include Grok Bot team toggles; unverified, review each):"
if [ -n "${review}" ]; then echo "${review}"; else echo " none"; fi
if [ -n "${HTH_AUDIT_OUT:-}" ]; then
cp "${events}" "${HTH_AUDIT_OUT}"
echo " exported ${count} event(s) as JSONL to ${HTH_AUDIT_OUT}"
fi
if [ "${count}" -eq 0 ]; then
echo " NOTE: zero events — confirm the window, and that this key belongs to the Enterprise team"
fi
if [ -n "${flagged}" ] && [ "${HTH_FAIL_ON_FLAGGED:-0}" = "1" ]; then
gb_finding "$(wc -l <<<"${flagged}" | tr -d ' ') posture-changing Grok Bot event(s) in the window"
fi
GB_UNPROVEN="changes before the ${days}-day window, and Grok Bot toggles that log only as team_settings under an undocumented setting_name (listed above for review, TRAP 3)"
}
Code Pack: Sigma Detection Rules (2)
# Admin API pull shape (event_type + event_data.<field>). trigger_type values are
# not enumerated, so the rule does not filter on them.
detection:
selection:
event_type: 'grok_bot_routine'
event_data.action:
- 'create'
- 'update'
- 'enable'
condition: selection
fields:
- timestamp
- user_email
- ip_address
- application_type
- event_data.automation_id
- event_data.name
- event_data.sand_agent_id
- event_data.trigger_type
- event_data.creation_source
- event_data.enabled
# Admin API pull shape (event_type + event_data.<field>). Family values are not
# enumerated, so other destination updates are reviewed in the 6.02 pack.
detection:
selection_deleted:
event_type: 'customer_telemetry_destination'
event_data.action: 'delete'
selection_disabled:
event_type: 'customer_telemetry_destination'
event_data.enabled:
- false
- 'false'
selection_content_on:
event_type: 'customer_telemetry_content_opt_in'
event_data.enabled:
- true
- 'true'
condition: 1 of selection_*
fields:
- timestamp
- user_email
- ip_address
- event_type
- event_data.action
- event_data.destination_id
- event_data.endpoint_url
- event_data.enabled
- event_data.enabled_families
- event_data.disabled_families
Validation & Testing
- The pack exports the Grok Bot event set for the window, and its count for each event type matches the Audit Log page filtered by the same event type over the same window
- Creating a test routine fires the SIEM alert within the pull or stream interval, and so does editing it
Expected result: Every Grok Bot posture change and every new routine reaches the SIEM.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 8.2 | Collect Audit Logs |
| CIS Controls v8 | 8.9 | Centralize Audit Logs |
| CIS Controls v8 | 8.11 | Conduct Audit Log Reviews |
| NIST 800-53 | AU-2 | Event Logging |
| NIST 800-53 | AU-6 | Audit Record Review, Analysis, and Reporting |
| NIST 800-53 | SI-4 | System Monitoring |
| SOC 2 | CC7.2 | Monitoring of system components for anomalies |
| ISO 27001:2022 | A.8.15 | Logging |
| ISO 27001:2022 | A.8.16 | Monitoring activities |
| Product benchmark | — | No product benchmark exists yet |
7. Lifecycle & Containment
7.1 Offboard Completely: Delete Computer Data, Remove Access, Revoke Sessions
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls v8 | 5.3, 6.2 |
| NIST 800-53 | AC-2, PS-4, MP-6 |
Description
Offboard in three steps, in order: on Enterprise, delete the member’s computer data while they are still on the team; remove their access on every team linked to the organization; then revoke their IdP sessions.
Why terminating is not enough: terminating a computer, manually or automatically, ends current work but keeps the durable disk with the member’s Bots, files and logins: “None of these remove access” (Grok Bot for teams and enterprises).
The three parts, in order:
- Delete computer data (Enterprise). Do it while the member is still on the team. It works only for current members: “Every ID must belong to a current member of the team who has signed in to Cursor; if one doesn’t, the request returns 400 and nothing starts”
- Remove access by team removal, SCIM deprovisioning (cursor 1.4) or group removal. Remove the member from every team linked to the organization, because one computer spans all of them
- Revoke the member’s IdP sessions, because application sessions persist on the computer until revoked
Rationale
Why This Matters:
- The durable disk keeps “local files, browser sessions, and anything saved in the browser”, and command-line credentials are shared across all of a member’s Bots on that computer. “Deleting a Bot doesn’t remove computer files or browser sessions.” (Cursor: Grok Bot security)
- Connector OAuth tokens stay on Cursor’s backend (“tokens are never stored on the computer”), so revoke those grants at each source service
- Once a member is gone, no admin path to delete their durable data is documented. The fallback is the DPA, under which data is “deleted or returned within 30 days of written direction after the service ends”
Attack Prevented: Orphaned standing sessions and credentials on a departed member’s hosted computer
Prerequisites
- Enterprise: an organization admin for Manage Bot Computers, or an Organization API key with
admin:*and computer operations turned on for the team by the account team - Self-serve Teams: admins have no documented data-deletion path, because Manage Bot Computers is Enterprise only. Individual-plan users delete through Cursor’s account settings flow under the applicable Cursor terms
- Team Bots the leaver owns: the Team Bots docs do not say what happens to a Team Bot when its owner leaves the team, and they offer no ownership transfer. Managers can change a Bot’s setup, but “Publishing, unpublishing, and deleting the Bot stay with” the owner (Team Bots)
- Owner dependencies: several parts of a Team Bot depend on the owner. Its Slack setup “uses the owner’s Slack connection”, and Slack posts from unlinked senders “count against the owner”
- Before removing a member: list the Team Bots they own in Manage Team Bots, and have the owner delete each one or delete it as an admin (trash icon, type the exact name, Delete). Admin deletion also removes the Bot’s Slack app; “If the Slack app can’t be removed, a Slack workspace admin can remove it in Slack.”
- Do not rely on Unpublish: only the owner can publish again, so a Bot unpublished by a departing owner can afterwards only be deleted (5.2, 8.6)
ClickOps Implementation
Run the steps in this order.
Step 1: Delete the data first, while the member is still on the team (Enterprise, organization admins)
- Navigate to: Cursor dashboard → Grok Bot → Bot Computers → Manage Bot Computers → Manage
- Search for and check the member → Manage selected → Delete VMs and Data
- Watch the progress card until the member’s row is complete, then select Done. A dashboard start where no selected member is still on the team shows Not Started
Step 2: Remove access
- Remove the member from every Cursor team linked to the organization
- With SCIM, unassigning the user in the IdP removes them from Cursor automatically, so do it only after Step 1
- On Enterprise you can also take the member out of the group allowed by Manage Group Access (beside Enable Grok Bot) rather than dropping the whole group. On self-serve Teams, removing the member from the team is the only route
Step 3: Revoke sessions and grants
- Revoke the user in your IdP, which ends their application sessions on the computer
- Delete their Team Bots, or have them deleted (5.2)
- Revoke connector grants at each source service
The dashboard path is transcribed from Manage Grok Bot computers.
Code Implementation
The pack is dry-run by default. Its sequence:
- Same-team proof: before anything irreversible, in both modes, it proves the two keys point at the same team. The Team key’s
GET /teams/model-access/configurationreturns the “Integer team ID implied by the API key”; where that route is not available, the pack compares the Team key’s active members with the Organization API’s members ofCURSOR_TEAM_ID - Leaver check: it requires the leaver to be an active member on the Team key’s team, so remove-member cannot fail after the delete
- Numeric id: it resolves the member’s numeric id with
GET /organizations/members?teamId= - Delete: it starts
delete_vm_and_datathroughPOST /organizations/teams/:teamId/grok-bot/operationswith a freshoperationId - Poll: it polls
GET /organizations/teams/:teamId/grok-bot/operations/:operationIdand checks that the polled operation is this member’sdelete_vm_and_databefore trusting the member’s item - Remove: only when that item succeeds does it call
POST /teams/remove-memberand print the IdP revocation reminder
The Organization API notes:
- Operations are turned on per team by the account team, and every route returns 403 until then
- Only an Organization key with
admin:*works; other scopes return 401 - A queued operation returns 202, and 409 means another operation is running. Retrying with the same
operationIdreturns the existing operation whatever its action: “Cursor doesn’t compare the rest of the body on a retry, so generate a new UUID for every new operation”. The pack resumes only through its own variable,HTH_OFFBOARD_OPERATION_ID, so an id exported for a 7.2 terminate can never stand in for a delete userIdsare numeric ids fromGET /organizations/members?teamId=, not the encodeduser_…ids from/teams/membersdelete_vm_and_datais capped at 1,000 members and cannot be undone.items[]is null for operations of more than 1,000 members
Remove-member: POST /teams/remove-member (Admin API) is Enterprise only and takes a Team key. It takes an encoded user_… id or an email, never both, and at least one paid member and one admin must remain.
Audit evidence comes from:
grok_bot_vm_bulk(bulk_permanent_delete, with counts only and no user ids; “Its child operations emit no rows”)remove_usercredentials_revoked(sessionsrevoked,retainedorrevoke_failed)
Per-member proof of the delete comes from the operation’s items[].
Code Pack: API Script
offboard() {
local members uid op body status item state active
# 0. Both keys must point at CURSOR_TEAM_ID, and the leaver must be active on
# the Team key's team, before anything irreversible is planned (TRAP 6).
gb_require_team_key_matches
gb_get team /teams/members
gb_require "GET /teams/members" 200
gb_shape '.teamMembers | type == "array"' "GET /teams/members"
active=$(jq -r --arg e "${HTH_OFFBOARD_EMAIL}" '[.teamMembers[] | select((.email | ascii_downcase) == ($e | ascii_downcase) and .isRemoved == false)] | length' "${GB_BODY_FILE}")
if [ "${active}" -ne 1 ]; then
echo "PRECONDITION: ${HTH_OFFBOARD_EMAIL} is not exactly one active member of the Team key's team (found ${active}) — remove-member would fail after an irreversible delete (TRAP 6)" >&2
exit 2
fi
# 1. Numeric id for the Organization API, resolved by email (TRAP 3).
members="$(gb_tmp members)"
gb_org_members "${members}"
uid=$(jq -r --arg e "${HTH_OFFBOARD_EMAIL}" 'select((.email | ascii_downcase) == ($e | ascii_downcase)) | .id' "${members}")
if [ -z "${uid}" ] || [ "$(wc -l <<<"${uid}" | tr -d ' ')" -ne 1 ]; then
echo "PRECONDITION: ${HTH_OFFBOARD_EMAIL} is not exactly one member of team ${CURSOR_TEAM_ID} — data can only be deleted for a current member (TRAP 1)" >&2
exit 2
fi
echo "Grok Bot 7.1 — offboarding ${HTH_OFFBOARD_EMAIL} (numeric id ${uid}) from team ${CURSOR_TEAM_ID}"
op=$(gb_uuid HTH_OFFBOARD_OPERATION_ID)
# A resumed id must name THIS member's delete, or be unused (TRAP 7). The
# status route documents two 404 bodies: "Operation not found." (never
# started, or aged out) and "This operation finished, and its status is no
# longer available." (completion can no longer be proven).
if [ -n "${HTH_OFFBOARD_OPERATION_ID:-}" ]; then
gb_get org "/organizations/teams/${CURSOR_TEAM_ID}/grok-bot/operations/${op}?limit=1000"
case "${GB_CODE}" in
200)
if ! jq -e --argjson u "${uid}" '.action == "delete_vm_and_data" and ((.items == null) or any(.items[]; .userId == $u))' "${GB_BODY_FILE}" >/dev/null; then
echo "PRECONDITION: HTH_OFFBOARD_OPERATION_ID ${op} is a $(jq -r '.action // "unknown"' "${GB_BODY_FILE}") operation that does not cover ${HTH_OFFBOARD_EMAIL} — unset it to start a new delete; the member was NOT removed (TRAP 7)" >&2
exit 2
fi ;;
404)
if jq -e '(.message // .error // "") | tostring | test("no longer available")' "${GB_BODY_FILE}" >/dev/null 2>&1; then
echo "PRECONDITION: operation ${op} finished and its status is no longer available, so the delete cannot be proven — the member was NOT removed; unset HTH_OFFBOARD_OPERATION_ID to start a new delete (TRAP 7)" >&2
exit 2
fi ;;
*) gb_require "GET /organizations/teams/${CURSOR_TEAM_ID}/grok-bot/operations/${op}" 200 ;;
esac
fi
body=$(jq -nc --argjson u "${uid}" --arg op "${op}" '{action: "delete_vm_and_data", userIds: [$u], operationId: $op}')
if [ "${GB_APPLY}" -ne 1 ]; then
gb_plan POST "/organizations/teams/${CURSOR_TEAM_ID}/grok-bot/operations" "${body}"
gb_plan POST /teams/remove-member "$(jq -nc --arg e "${HTH_OFFBOARD_EMAIL}" '{email: $e}')"
echo "DRY RUN: nothing was changed. Re-run with --apply. delete_vm_and_data cannot be undone (TRAP 2)."
return 0
fi
# 2. Delete the computer and its durable data. A retry with the same
# operationId returns the existing operation instead of starting another.
gb_post org "/organizations/teams/${CURSOR_TEAM_ID}/grok-bot/operations" "${body}"
if [ "${GB_CODE}" = "409" ]; then
echo "PRECONDITION: another computer operation is running for this team: $(jq -r '.runningOperationId // "unknown (use GET .../operations/latest)"' "${GB_BODY_FILE}")" >&2
exit 2
fi
gb_require "POST /organizations/teams/${CURSOR_TEAM_ID}/grok-bot/operations" 202
op=$(jq -r '.operationId // empty' "${GB_BODY_FILE}")
if [ -z "${op}" ]; then echo "PRECONDITION: 202 without an operationId" >&2; exit 2; fi
echo " delete_vm_and_data queued: operationId=${op} (re-run with HTH_OFFBOARD_OPERATION_ID=${op} to resume)"
# 3. Follow it to the end and read THIS member's result.
status="$(gb_tmp operation)"
gb_poll_operation "${op}" "${status}" HTH_OFFBOARD_OPERATION_ID
# A reused operationId returns the EXISTING operation and Cursor does not
# compare the body, so prove this is our delete before trusting its items (TRAP 7).
if ! jq -e --arg op "${op}" '.operationId == $op and .action == "delete_vm_and_data"' "${status}" >/dev/null; then
echo "PRECONDITION: operation ${op} is action=$(jq -r '.action // "unknown"' "${status}"), not delete_vm_and_data — HTH_OFFBOARD_OPERATION_ID names a different operation; unset it and re-run. The member was NOT removed (TRAP 7)" >&2
exit 2
fi
item=$(jq -c --argjson u "${uid}" '(.items // [])[] | select(.userId == $u)' "${status}")
if [ -z "${item}" ]; then item='{}'; fi
state=$(jq -r '.state // "missing"' <<<"${item}")
echo " member result: state=${state} reason=$(jq -r '.reason // "none"' <<<"${item}")"
if [ "${state}" != "succeeded" ]; then
gb_fail "computer data was NOT deleted for ${HTH_OFFBOARD_EMAIL} — refusing to remove the member, because removal would end the only documented deletion path (TRAP 1)"
fi
# 4. Only now remove the member from the team.
gb_post team /teams/remove-member "$(jq -nc --arg e "${HTH_OFFBOARD_EMAIL}" '{email: $e}')"
gb_require_2xx "POST /teams/remove-member"
if jq -e '.success == false' "${GB_BODY_FILE}" >/dev/null 2>&1; then
gb_fail "remove-member answered success=false: $(jq -c '.' "${GB_BODY_FILE}")"
fi
# 5. Confirm, rather than trust the response.
gb_get team /teams/members
gb_require "GET /teams/members" 200
gb_shape '.teamMembers | type == "array"' "GET /teams/members"
if jq -e --arg e "${HTH_OFFBOARD_EMAIL}" 'any(.teamMembers[]; (.email | ascii_downcase) == ($e | ascii_downcase) and .isRemoved == false)' "${GB_BODY_FILE}" >/dev/null; then
gb_fail "${HTH_OFFBOARD_EMAIL} is still listed as an active team member"
fi
echo "DONE: computer data deleted and ${HTH_OFFBOARD_EMAIL} removed from team ${CURSOR_TEAM_ID}."
echo " Next: confirm audit rows remove_user and credentials_revoked (sessions=revoked; 'retained' means"
echo " another org team still holds them — TRAP 5), offboard any other team, and revoke IdP sessions."
}
Validation & Testing
- The operation reports
succeededfor the member, andgrok_bot_vm_bulkshowsbulk_permanent_deletewith matching counts GET /teams/membersthen showsisRemovedtrueon every linked team- The audit log shows
remove_userandcredentials_revokedwithsessionsrevoked.retainedmeans the member still belongs to another team in the organization, so sessions and repository grants stay valid - The member’s next sign-in fails, and the IdP shows their sessions revoked
Expected result: A leaver’s computer data is gone, their access is removed on every linked team, and their sessions are revoked.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 5.3 | Disable Dormant Accounts |
| CIS Controls v8 | 6.2 | Establish an Access Revoking Process |
| NIST 800-53 | AC-2 | Account Management |
| NIST 800-53 | PS-4 | Personnel Termination |
| NIST 800-53 | MP-6 | Media Sanitization |
| SOC 2 | CC6.2 | User registration and authorization |
| SOC 2 | CC6.5 | Discontinuation of logical and physical protections over disposed assets |
| ISO 27001:2022 | A.5.18 | Access rights |
| ISO 27001:2022 | A.6.5 | Responsibilities after termination or change of employment |
| ISO 27001:2022 | A.8.10 | Information deletion |
| Product benchmark | — | No product benchmark exists yet |
7.2 Prepare a Grok Bot Containment Runbook
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls v8 | 17.4 |
| NIST 800-53 | IR-4, IR-8 |
Description
Write and rehearse a runbook for a hijacked Bot or a vendor-side incident, in five steps:
- Disable Grok Bot or narrow Manage Group Access
- Disabling blocks every member without deleting computers
- An empty limited list is a 400, so use disable to block everyone
- No page says whether disabling stops a Bot that is already working, so terminate as well (step 2)
- Terminate affected computers to stop running Bots and in-flight routine runs (“Running work stops”). No page says that Terminate, the disable switch or removing access stops a scheduled routine from firing again
- Have each affected routine’s creator pause or delete it in their own chat (Bot → View conversation details → Routines; Skills, routines, and automations). Revoke IdP sessions first (step 4) so that the legitimate member, not the attacker, does this cleanup
- Routines are personal: “only they can see or change it” (Team Bots), and on a Team Bot that includes teammates’ routines, which the Bot’s owner and managers cannot touch
- Admins have no routine switch or API
- For a hijacked Team Bot, an admin can delete it from Manage Team Bots (trash icon, type the exact name, Delete), which removes the Bot “for your whole team: teammates lose it, along with their chats and routines on it. This can’t be undone.”
- Only its member can delete a personal Bot, and deleting a Bot removes its routines
- Revoke IdP sessions, and revoke plugin authorization at each source service
- Choose between Recreate, which keeps files and logins, and Delete VMs and Data, which is a true reset
Keep two keys ready:
- an Organization API key with
admin:*for computer operations, which the account team must turn on per team - a Team API key with
admin:*for disable and access
Rationale
Why This Matters:
- Bots keep working while laptops are closed, and routines fire unattended
- Disabling Grok Bot blocks access, but no page says it stops work already running. Terminate is the documented way to stop running Bots (“Running work stops”, Enterprise and organization admins only), and routines need their own step
- On self-serve Teams there is no switch and no Manage Bot Computers, so removing the member is the only documented way to end a member’s access. Removing a Teams seat ends the Teams grant, but a personal SuperGrok or X Premium+ link on the same Cursor account is a separate grant
- Teams admins can still block connectors (2.1), set local execution to Never allow (2.4), turn off Cloud Agents (2.3) and revoke IdP sessions
Attack Prevented: Continued autonomous action during an active compromise
Prerequisites
- Enterprise: an organization admin, plus both API keys above
- A dedicated pilot team for rehearsal, because disable applies to the whole team
ClickOps Implementation
Step 1: Stop access
- Team-wide (Enterprise only): Cursor dashboard → Grok Bot → Enable Grok Bot (switch) → Off
- Per cohort (Enterprise): Manage Group Access, beside the switch
Step 2: Stop running work (Enterprise, organization admins only)
- Cursor dashboard → Grok Bot → Bot Computers → Manage Bot Computers → Manage
- Search and check members → Manage selected → Terminate VMs
- On the confirmation screen select Terminate VMs → Done. The same menu also holds Recreate VMs and Delete VMs and Data (Manage Grok Bot computers: Run an operation)
Step 3: Revoke, then stop routines
- Revoke IdP sessions and each plugin’s authorization at its source service, so the legitimate member, not the attacker, does the routine cleanup
- Member (each routine’s creator): View conversation details → Routines → pause or delete each routine
- Admin, Team Bot only: Manage Team Bots → Delete (irreversible; removes every teammate’s routines on that Bot)
- Admin, verify: pull
grok_bot_routineaudit rows. Actiondisableordeleteon each affectedsand_agent_idandautomation_idconfirms the cleanup;create,updateorenableafterwards means re-arming. On Enterprise with Action Recording and OpenTelemetry Export (6.1),cursor.grok_bot.routine_runrows after containment show a routine still firing
Code Implementation
The pack is dry-run by default. It can disable Grok Bot, terminate computers or both, and it polls operations to completion.
- Keys: it reads the Team key and the Organization key from separate environment variables. When it is asked to disable and terminate together, it first proves both keys point at
CURSOR_TEAM_ID(as in 7.1) - Resume: it resumes only through its own
HTH_CONTAIN_OPERATION_ID. It checks that the polled operation is aterminate_vmcovering exactly the requested members before it reports “terminated”, because a reusedoperationIdreturns whatever operation already holds it - Status: its
statuscheck reports a 404 from the latest-operation route as confirmed only when the team is also linked to the organization, because that route returns 404 both before any operation has run and for an unlinked team
The calls:
- Disable:
POST /grok-bot/disablewith a Team key holdingadmin:*. It returns 204, and it “Returns 403 on Teams plans”. Aread:*key returns 401, and a 401 can also mean “Grok Bot Admin API not enabled for the team” - Narrow:
PUT /grok-bot/accessreturns 403 when group access is not available, and 400 for an empty limited list - Terminate:
POST /organizations/teams/:teamId/grok-bot/operationswith actionterminate_vm, up to 25,000 numericuserIds, and anoperationId. It needs an Organization key withadmin:*, turned on per team: until then the API answers “Bot fleet admin API access is not enabled for this team”. It returns 409 withrunningOperationIdwhile another operation runs - Terminate preconditions: every id “must belong to a current member of the team who has signed in to Cursor; if one doesn’t, the request returns
400and nothing starts”, so withHTH_CONTAIN_ALL=1one unqualified member stops the whole terminate. List Organization Members exposes no signed-in field, so the pack cannot pre-filter. On a 400 it says nothing started and names the remedies (re-run, narrow withHTH_CONTAIN_EMAILS, or use Terminate VMs in the dashboard). RehearseHTH_CONTAIN_ALLon the pilot team - Poll:
GET .../operations/:operationIdor.../operations/latest. The latter returns 404 until an operation has run, and per-member items exist only for operations of 1,000 members or fewer
Detection:
- Sigma rule: it alerts on
sand_onboardingwithnew_completedfalse(disable), ongrok_bot_vm_bulk(bulk_kill,bulk_recreate,bulk_permanent_delete), and ongrok_bot_vmwith actionkill - Event timing:
grok_bot_vm_bulkis emitted when a bulk operation completes, and its child operations emit no rows - Field paths: streamed events nest fields under the event-type key (for example
sand_onboarding.new_completed); Admin API pulls useevent_data
Code Pack: API Script
disable_team() {
if [ "${GB_APPLY}" -ne 1 ]; then
gb_plan POST /grok-bot/disable
echo " (team-wide: every member loses Grok Bot access — TRAP 1)"
return 0
fi
gb_post team /grok-bot/disable
gb_require "POST /grok-bot/disable" 204
echo " POST /grok-bot/disable -> 204"
gb_get team /grok-bot/capabilities
gb_require "GET /grok-bot/capabilities" 200
if [ "$(jq -r '.enabled' "${GB_BODY_FILE}")" != "false" ]; then
gb_fail "Grok Bot still reads enabled=$(jq -r '.enabled' "${GB_BODY_FILE}") after disable"
fi
echo " confirmed: enabled=false (audit: sand_onboarding with new_completed=false)"
}
terminate_computers() {
local members ids n op body opfile failed got
gb_require_key org
members="$(gb_tmp members)"
gb_org_members "${members}"
if [ "${HTH_CONTAIN_ALL:-0}" = "1" ]; then
ids=$(jq -s -c 'map(.id) | unique' "${members}")
elif [ -n "${HTH_CONTAIN_EMAILS:-}" ]; then
ids=$(jq -s -c --arg e "${HTH_CONTAIN_EMAILS}" '
($e | ascii_downcase | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $want
| [.[] | select((.email | ascii_downcase) as $m | $want | index($m))] | map(.id) | unique' "${members}")
if [ "$(jq 'length' <<<"${ids}")" -ne "$(jq -n --arg e "${HTH_CONTAIN_EMAILS}" '$e | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0)) | unique | length')" ]; then
echo "PRECONDITION: not every address in HTH_CONTAIN_EMAILS is a member of team ${CURSOR_TEAM_ID}" >&2
exit 2
fi
else
echo "PRECONDITION: terminate needs HTH_CONTAIN_EMAILS or HTH_CONTAIN_ALL=1" >&2
exit 2
fi
n=$(jq 'length' <<<"${ids}")
if [ "${n}" -lt 1 ] || [ "${n}" -gt 25000 ]; then
echo "PRECONDITION: terminate_vm takes 1-25,000 members (got ${n})" >&2
exit 2
fi
op=$(gb_uuid HTH_CONTAIN_OPERATION_ID)
body=$(jq -nc --argjson u "${ids}" --arg op "${op}" '{action: "terminate_vm", userIds: $u, operationId: $op}')
if [ "${GB_APPLY}" -ne 1 ]; then
gb_plan POST "/organizations/teams/${CURSOR_TEAM_ID}/grok-bot/operations" "${body}"
return 0
fi
gb_post org "/organizations/teams/${CURSOR_TEAM_ID}/grok-bot/operations" "${body}"
if [ "${GB_CODE}" = "409" ]; then
echo "PRECONDITION: another computer operation is running for this team: $(jq -r '.runningOperationId // "unknown (run: status)"' "${GB_BODY_FILE}")" >&2
exit 2
fi
if [ "${GB_CODE}" = "400" ]; then
echo "PRECONDITION: POST .../grok-bot/operations returned 400 ($(jq -r '(.message // .error // "no message") | tostring' "${GB_BODY_FILE}" | head -c 300)) — NOTHING STARTED, no computer was terminated." >&2
echo " Documented causes: a listed member is no longer a current member of the team (for example removed by SCIM or the dashboard after the list was read) or never signed in to Cursor, or the body is malformed or too large (TRAP 3)." >&2
echo " Remedies: re-run (the member list is re-read every run); narrow with HTH_CONTAIN_EMAILS; or use Grok Bot Computers > Manage > select members > Terminate VMs in the dashboard." >&2
if [ "${DO_DISABLE}" -eq 1 ]; then
echo " The disable already landed, but running computers have NOT been stopped (TRAP 1)." >&2
fi
exit 2
fi
gb_require "POST /organizations/teams/${CURSOR_TEAM_ID}/grok-bot/operations" 202
op=$(jq -r '.operationId // empty' "${GB_BODY_FILE}")
if [ -z "${op}" ]; then echo "PRECONDITION: 202 without an operationId" >&2; exit 2; fi
# TRAP 5: a reused operationId returns whatever operation already holds it.
# Check the action before following it, so a responder never waits on the wrong one.
gb_get org "/organizations/teams/${CURSOR_TEAM_ID}/grok-bot/operations/${op}?limit=1"
gb_require "GET /organizations/teams/${CURSOR_TEAM_ID}/grok-bot/operations/${op}" 200
if [ "$(jq -r '.action // ""' "${GB_BODY_FILE}")" != "terminate_vm" ]; then
echo "PRECONDITION: operationId ${op} is an existing $(jq -r '.action // "unknown"' "${GB_BODY_FILE}") operation, not this terminate (Cursor returns 202 for an existing operationId without comparing the body) — unset HTH_CONTAIN_OPERATION_ID and re-run" >&2
exit 2
fi
echo " terminate_vm queued for ${n} member(s): operationId=${op} (re-run with HTH_CONTAIN_OPERATION_ID=${op} to resume)"
opfile="$(gb_tmp operation)"
gb_poll_operation "${op}" "${opfile}" HTH_CONTAIN_OPERATION_ID
# The operation must cover exactly the members requested (TRAP 5).
if jq -e '.items != null' "${opfile}" >/dev/null; then
got=$(jq -c '[.items[].userId] | unique' "${opfile}")
if [ "${got}" != "$(jq -c 'sort' <<<"${ids}")" ]; then
echo "PRECONDITION: operation ${op} covers userIds ${got}, not the requested ${ids}" >&2
exit 2
fi
elif [ "$(jq -r '.counts.total // -1' "${opfile}")" != "${n}" ]; then
echo "PRECONDITION: operation ${op} has counts.total=$(jq -r '.counts.total // "?"' "${opfile}") but ${n} members were requested" >&2
exit 2
fi
jq -r '(.items // [])[] | select(.state != "succeeded") | " userId=\(.userId) \(.state) \(.reason // "")"' "${opfile}"
failed=$(jq -r '.counts.failed // 0' "${opfile}")
if [ "${failed}" -gt 0 ]; then
gb_fail "${failed} member computer(s) were not terminated — start a new operation for them"
fi
echo " terminated: state=$(jq -r '.state' "${opfile}") counts=$(jq -c '.counts' "${opfile}") (audit: grok_bot_vm_bulk bulk_kill)"
}
Code Pack: Sigma Detection Rule
# Admin API pull shape (event_type + event_data.<field>). grok_bot_vm_bulk is one
# row per completed operation; its child operations emit no rows.
detection:
selection_disabled:
event_type: 'sand_onboarding'
event_data.new_completed:
- false
- 'false'
selection_bulk:
event_type: 'grok_bot_vm_bulk'
selection_kill:
event_type: 'grok_bot_vm'
event_data.action: 'kill'
condition: 1 of selection_*
fields:
- timestamp
- user_email
- ip_address
- event_type
- event_data.action
- event_data.operation_id
- event_data.target_count
- event_data.succeeded_count
- event_data.skipped_count
- event_data.failed_count
- event_data.target_user_id
- event_data.target_user_email
- event_data.old_completed
- event_data.new_completed
- event_data.source
Validation & Testing
- Run a tabletop on a dedicated pilot team, because disable applies to the whole team
- The pack’s dry run lists the intended calls
- A live terminate reports per-member results and emits
grok_bot_vm_bulkbulk_kill - Before disabling, start a long task on a pilot Bot. Disable returns 204 and pilot members lose access; record whether the Bot that was mid-task stops before Terminate, since the docs do not say
- Afterwards, the members pause or delete the pilot Bots’ routines, and
grok_bot_routinedisableordeleterows confirm it
Expected result: A rehearsal on the pilot team proves each runbook step: access is blocked, running work stops, routines are paused or deleted by their creators, and sessions are revoked.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 17.4 | Establish and Maintain an Incident Response Process |
| NIST 800-53 | IR-4 | Incident Handling |
| NIST 800-53 | IR-8 | Incident Response Plan |
| SOC 2 | CC7.4 | Incident response |
| ISO 27001:2022 | A.5.24 | Information security incident management planning and preparation |
| ISO 27001:2022 | A.5.26 | Response to information security incidents |
| Product benchmark | — | No product benchmark exists yet |
7.3 Turn On Terminate Inactive Computers
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls v8 | 4.1 |
| NIST 800-53 | CM-2 |
Description
Turn on Terminate Inactive Computers, which terminates a member’s computer after 30 days without use. It is Enterprise only, set by organization admins, and off by default.
Why: “A hibernated computer stays around until someone terminates it, even when the member has moved on or stopped using Grok Bot.”
Timing: “Computers already past 30 days are included. There is no grace period.” Termination lands in the days after the 30-day mark on a rolling schedule, and a computer that is awake is never terminated.
Scope: the setting applies to a member’s computer if any of their Enterprise teams has it on.
What a terminate does: an automatic terminate works like a manual one. The durable disk stays, and the member’s next message starts a fresh computer with their Bots, files and logins in place. Apps and packages members installed themselves are removed, and Team Setup runs again.
What it is not: this is idle-computer hygiene, not access removal or data deletion; for leavers, use 7.1.
Rationale
Why This Matters:
- Idle hibernated computers accumulate, and each one keeps self-installed software that drifts from the managed Team Setup baseline
- Termination clears that drift and re-applies Team Setup on next use
- It does not end access or sessions: the durable disk keeps browser sessions and logins, and “Terminating does not remove access” (Manage Grok Bot computers)
Attack Prevented: Drift and sprawl of idle computers carrying unmanaged software. It does not, on its own, prevent an attack on a member account; access and session risk is handled by 7.1 and IdP session revocation
ClickOps Implementation
Step 1: Turn the setting on (Enterprise, organization admins)
- Navigate to: Cursor dashboard → Grok Bot → Bot Computers → Terminate Inactive Computers
- Turn it on. A confirmation opens → Turn On
Doc conflict: placement and name. The two docs disagree on placement. Confirm live.
- xAI: its Manage Grok Bot computers page puts the toggle “just above Manage Bot Computers in the same section”, in a section it calls Bot Computers
- Cursor: its teams page puts it “below it”, meaning below the bulk control. Cursor uses Grok Bot Computers both as that bulk control’s name and, on its computers page (“below Grok Bot Computers on the same dashboard page”), as the container for both features
Automation: ClickOps only — Cursor (the Grok Bot admin plane) exposes no write interface for this setting (Admin API, Grok Bot, 2026-10-08). It is not a field of PATCH /grok-bot/capabilities, and it is not an Organization API operation action (Organization API, Grok Bot computers, 2026-10-08).
- Not readable either:
GET /grok-bot/capabilitiesdoes not return it - Manual equivalent: the Organization API’s
terminate_vm(7.2) is a scriptable manual equivalent of a single cleanup run, not the setting - No detection: no documented audit event distinguishes automatic termination, and the toggle’s
team_settingssetting_nameis undocumented, so no detection is authored for it
Validation & Testing
- The Terminate Inactive Computers toggle is on in Bot Computers
- A pilot member whose computer has slept for more than 30 days finds, in the days after the mark, that their next message starts a fresh computer, with self-installed packages gone and Team Setup re-applied
Expected result: No member computer stays idle beyond roughly 30 days with unmanaged software on it.
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| CIS Controls v8 | 4.1 | Establish and Maintain a Secure Configuration Process |
| NIST 800-53 | CM-2 | Baseline Configuration |
| SOC 2 | CC8.1 | Change management |
| ISO 27001:2022 | A.8.9 | Configuration management |
| Product benchmark | — | No product benchmark exists yet |
8. Known Gaps and Member Guidance
This section is reference material, not a set of controls, and it adds no cheat-sheet rows. It records the risks the vendor documents but gives admins no control over, the documentation conflicts this guide could not resolve without a live tenant, and the member-level settings that matter.
8.1 Surfaces With No Admin Control
| Surface | What the docs say | Nearest lever |
|---|---|---|
| Tagging @bot on X | Tagging @bot in a post hands it to the member’s main Bot, and X tells the member that “tagged requests start work automatically”. It needs a Grok account with X connected, Grok Bot access on that account, and “At least one Bot of your own”; “Bots that someone shares with you can’t take tagged requests”, so a teammate’s Team Bot is not reachable this way. It is “not yet available for accounts based in Australia, the United Kingdom, the European Union, Iceland, Liechtenstein, or Norway”. No admin setting disables it (Tag @bot on X) |
Network Controls (3.1), Enforce Auto-review (4.1), Action Recording (6.1) |
| Routines and Slack or webhook triggers | On a Team Bot, routines are personal: a routine “runs as” the member who set it up, “and only they can see or change it” (Team Bots). A Bot template carries its routines, and “A public link can be opened by anyone who has it and shows the Bot’s shared configuration, including its identity, description, skills, and routines” (Create and manage Bots). There is no allowlist, disable switch or API. A Bot can own up to 50 routines, and schedules must be at least five minutes apart (Skills, routines, and automations). Routines run “while your laptop is closed” (Cursor help: Routines) | Public template sharing off (5.1), grok_bot_routine audit events (6.2), routine_run over OpenTelemetry (6.1), and Auto Review on automation writes (4.1), whose approvals go to the member |
| Team Bot creation and publishing | No documented admin control disables it (as of 2026-10-09), and there is no Team Bot API | Manage Team Bots (5.2); Manage Group Access limits it indirectly on Enterprise (1.1) |
| Bot memory and Team Bot team memory | No admin view or purge. Auto Review does not review memory writes | Team Bot governance (5.2), offboarding (7.1) |
| Spend | “A separate Grok Bot spend cap is not available today.” Account-level on-demand controls apply | cursor 3.3 |
| Team Bot ownership transfer | Not documented, and nothing is documented about a Team Bot after its owner leaves the team | Owner deletion before departure, or admin deletion in Manage Team Bots (7.1) |
| Model restriction | docs.x.ai says the team model allowlist “is honored by default” but that “enforcement is not guaranteed” (Grok Bot security). cursor.com says “The team model allowlist does not govern Grok Bot” (Cursor: Grok Bot security), and “Grok Bot is not governed by your team’s Cursor model allowlist or blocklist” (Cursor help: models) | None. This guide writes no model-restriction control; review the sub-processor list with your account team |
8.2 Vendor-Admitted Design Gaps
The vendor documents these as absences. They are design gaps to weigh in a risk assessment, not settings to configure (Grok Bot security, Grok Bot security FAQ).
- No DLP hooks: “Dedicated data loss prevention hooks are not available.”
- No customer telemetry or EDR feed: Grok Bot “does not ship a built-in customer-facing telemetry or EDR feed”. Team Setup (3.3) is the path to install your own
- Shared egress: static egress IPs are shared across all Grok Bot customers, and dedicated per-customer IPs are not available
- Residency: computers run in the United States, but outside Cursor’s US-only data residency program by default
- Retention and restore: there is no per-organization retention policy and no customer-managed point-in-time restore of an individual computer
- Hosting: there is no on-premises, in-perimeter or bring-your-own-image deployment
- Identity: Bots act as the signed-in member rather than as distinct agent principals, and all of a user’s Bots share one computer. “Do not use separate Bots as a security boundary.” (Approvals, security, and privacy)
8.3 Changelog-Only Capabilities
These appear in the Grok Bot changelog and have no admin control (see Appendix B on how the changelog was read).
- V0.30.0 and V0.33.0: Link purchases on approval, and Pay with Link
- V0.53.0: 1Password Auto fill
- V0.64.0: a member-level Disable drafts for this Bot, which lets that Bot send email and Slack messages directly. Keep drafts on (8.7)
- V0.37.0: “Allow once works even when Execution on this computer is set to Never allow”. Test it live against the team ceiling in 2.4
8.4 Access Paths Outside Cursor-Team Governance
- Self-serve Grok Business seats (1.3): seat removal is the only documented lever. It is undocumented which Cursor team such a user lands in, and whether domain-verified SSO or the MDM allowed-team-ID policy applies to a Sign in with Grok session
- Personal SuperGrok, SuperGrok Plus, SuperGrok Heavy and X Premium+ links: no documented admin control, and such a link “can’t be unlinked or moved” to a different Cursor account once created (SuperGrok and Grok Bot)
- Grok Enterprise: access goes through the account manager, with no documented controls
- Sign-in domains: Cursor sign-in is moving to accounts.x.ai and accounts.spacex.ai, and the fallback is retired on October 30 (Sign-in domains; the page gives no year, and was read on 2026-10-09, so this is presumably October 30, 2026). Update proxy allowlists (cursor 9.2)
8.5 Audit Coverage Holes
No Grok-Bot-specific audit event or documented setting_name covers any of these:
- team network-policy changes
- the Enforce Auto-review toggle
- the
PATCH /grok-bot/capabilitiesfields - Team Secrets
- Terminate Inactive Computers
- Manage Team Bots assignment
- Team Bot managers, secrets, plugins and files (changelog V0.57.0 claims plugin activity is audited; see 8.6)
- Team Bot Slack apps
The generic team_settings event’s setting_name list is explicitly non-exhaustive, and it fires for team-wide changes “including changes made through the Admin API”, so some of these may land there. The 6.2 pack lists unrecognized setting_name values for review.
Whether team_rule covers Grok-Bot-scoped rules is inferred rather than documented. Audit streaming is set up by contacting hi@cursor.com (Compliance and monitoring).
Sigma rules: Cursor’s audit stream and OpenTelemetry export have no upstream Sigma taxonomy. This guide’s Sigma rules therefore declare a custom logsource and field mapping in each pack header.
- Field paths differ between streamed audit events, which nest fields under a key named after the event type, and Admin API pulls, which use
event_data - The rules match only documented
event_type, action and attribute values grok_bot_routinetrigger_typevalues andgrok_bot_group_settingssetting_namevalues are not enumerated, so no rule filters on them
8.6 Documentation Conflicts to Resolve Live
| Topic | Conflict | This guide’s position |
|---|---|---|
| Admin API reach on Teams | cursor.com/docs/api lists the Admin API as “Enterprise teams”. The Grok Bot section says /access, /network and /auto-review reads work “on every plan”, and documents 403 on Teams for disable and network writes. GET /grok-bot/capabilities is not in that every-plan read list, and no Teams behavior is documented for enable |
Writes are confirmed for Enterprise only. Every pack treats 401 and 403 as “not available to this team/plan” |
| Enterprise enablement | The plans page, which Cursor calls canonical, says “Consult your account executive to enable Grok Bot access for your team”. The teams page and the docs.x.ai mirror say “On the Enterprise plan, an admin turns it on from Grok Bot in the Cursor dashboard”. The Admin API’s POST /grok-bot/enable says “The first enable on an eligible Enterprise team starts the trial”. No page defines eligibility, the trial’s length, or what follows it |
Engage the account executive before rollout. Confirm live whether the dashboard switch is available to the team and whether the first enable starts a trial, and record that first enable as an approved change (1.1) |
| Entra policy precedence | xAI’s “Add a higher-priority policy” and “laptop sign-ins keep your existing requirements” versus Microsoft’s “all applicable policies must be satisfied” | Follow Microsoft. Treat the step 8 exclusions as the actual exception, record them, and pick the narrowest form (1.2, Step 2a) |
| Team Bot editors | xAI Team Bots: “Only the owner and the Bot’s managers can change this setup”, and “A manager can change the Bot’s setup, like its profile, skills, plugins, secrets, and Slack app”. Cursor help: Team Bots: “Only the owner can change a Team Bot”, with no manager role | Treat managers as real, since xAI documents Add manager under the Bot’s details → Setup → Managers. Confirm live that the Managers section exists (5.2, 7.1) |
| Team Bot after owner departure | The Team Bots page covers owner, manager and admin rights, but not what happens to a Team Bot, its secrets, its Slack app or its owner-billed Slack usage when the owner leaves the team | Delete the owner’s Team Bots before removing the member (7.1); confirm the post-removal behavior live |
| Plugin audit coverage | Changelog V0.57.0 says “Audit Logs record more Grok Bot activity, including … plugins”. The event table has no Grok Bot plugin event, and the generic mcp_authentication and mcp_server_config events are not documented to cover a Team Bot’s plugin changes |
Current docs win: Team Bot plugin changes are treated as unaudited (5.2, 8.5). Confirm live which event_type, if any, a Team Bot plugin change emits |
| Per-team enablement | The Admin API can return 401 “Grok Bot Admin API not enabled for the team”, and Organization API computer operations return 403 until the account team turns them on. The Organization API uses numeric user ids; the team API uses encoded user_… ids |
Packs handle both id types and both refusals |
| Group type for access | Manage Group Access links to Members > Groups; PUT /grok-bot/access takes billing groups |
Confirm the picker live (1.1) |
| Network allowlist size | Security page: “no cap on the number of entries”. API: “Up to 500 destinations” | The pack enforces 500 (3.1) |
| Template sharing values | The API lists all, team_only, none and null, but does not define none or map the dashboard’s Off |
The pack enforces team_only (5.1) |
| Auto-review rule semantics | The API’s allow/block map to the UI’s Allow automatically/Ask first only by inference. Empty lists keep stored rules only for teams that cannot set rules |
Rule authoring stays in the dashboard. The pack re-sends stored rules (4.1) |
| Action Recording and the Audit Log | Changelog V0.30.0 says recordings go to the audit log; the current docs say they do not | Current docs win (6.1) |
| Launch plan list | The 2026-08-11 launch post already lists plans that the 2026-08-26 post says were added that day | Cursor’s plans page is canonical (Overview) |
| UI labels | Bot Computers → Manage Bot Computers (xAI) vs Grok Bot Computers (Cursor); Terminate Inactive Computers “just above” vs “below” (7.3); Route traffic through this computer vs Route egress through this desktop, with different locked messages (3.2); Settings → Computer → Computers (xAI) vs Settings > Computer > Execution on this computer (Cursor) for the member’s local-execution setting (2.4); Settings → General → Auto-review vs the General > Bot section of Cursor’s settings page, and “Custom Rules” (4.1); Team Marketplace vs Teams Marketplace vs Connect Apps (2.1); Team Bots → Manage Team Bots (xAI) vs Manage Team Bots → Manage (Cursor), with Cursor help listing no None option (5.2); “Allow Cloud Agents” on a group tab is described only in prose (2.3); no documented labels for Team Rules (4.2) or public template sharing (5.1); Members & Groups vs Members > Groups (4.3) | Transcribe each live before rollout |
| Terraform | cursor/cursor 0.8.0 was published on 2026-10-08, the day of this census, and has no Grok Bot resources (registry) | Recheck at publish; any new resource changes the automation verdicts |
8.7 Member Guidance
These are member-level settings, not admin controls. Put them in your acceptable-use guidance and onboarding. The vendor’s own member baseline is in Grok Bot for teams and enterprises.
- Never paste credentials into chat. The masked secret request is the supported path. On a Team Bot, every secret is usable in any teammate’s conversation (5.2)
- Prefer Allow once over Always allow for actions that touch accounts, money or shared resources
- Size the shared browser to the task. Every Bot on the account shares one computer’s cookies, files and command-line credentials (Computer and apps). Sign the Bot’s browser out of accounts it no longer needs, and use scoped service accounts where the source system supports them
- Keep personal Auto-review rules. They are stored per desktop and synced to that desktop’s computer, under Settings → General → Auto-review
- Keep email and Slack drafts on. Do not use Disable drafts for this Bot (changelog V0.64.0)
- Clear remembered connector consent for Team Bots: Settings → General → Team Bots → Clear (Team Bots)
- Use hardware security key passthrough where available: Settings → General → Security Key → Use hardware security keys. It is on by default on macOS and Windows, is not yet supported on Linux, and asks for approval on every use
- Start new Bots on read-only tasks and drafts, and review installed plugins and active routines regularly. Pause a routine when its source system changes
9. Compliance Quick Reference
No product benchmark exists yet for Grok Bot. The mappings below are control-family mappings, not benchmark equivalents.
- CIS: the research pass found no CIS Benchmark for xAI, Grok, Grok Bot or Cursor. CIS’s only AI benchmark, MCP Server (1.0.0), is partially relevant to connectors but is not product coverage. The cisecurity.org catalog is a JavaScript shell to fetchers, so re-check it in a real browser before relying on this
- DISA: the public STIG Document Library (cyber.mil/stigs/downloads; the page is rendered by JavaScript and was checked in a real browser on 2026-10-09) lists no xAI, Grok or Cursor STIG. Searches for Grok, xAI and Cursor returned no results, while control searches for Kubernetes and Cisco returned 4 and 13. Documents limited to DoD CAC holders were not checked
- CISA SCuBA: it has no xAI or Grok baseline; its Entra ID baseline applies only to 1.2, as a labeled compatibility exception
SOC 2 Trust Services Criteria Mapping
| Control ID | Grok Bot Control | Guide Section |
|---|---|---|
| CC2.2 | Required Team Rules | 4.2 |
| CC6.1 | Access scoping, IdP exception, connectors, Google approval, Cloud Agents, local execution, Team Setup, Auto-review, Team Rules, template sharing, Team Bots | 1.1, 1.2, 2.1, 2.2, 2.3, 2.4, 3.3, 4.1, 4.2, 5.1, 5.2 |
| CC6.2 | Access scoping, Grok Business seats, offboarding | 1.1, 1.3, 7.1 |
| CC6.3 | Grok Business seats, Team Bots | 1.3, 5.2 |
| CC6.5 | Offboarding data deletion | 7.1 |
| CC6.6 | Network Controls, Allow Local Egress | 3.1, 3.2 |
| CC6.8 | Local execution ceiling | 2.4 |
| CC7.2 | Group override audit, Action Recording, control-plane audit | 4.3, 6.1, 6.2 |
| CC7.3 | Action Recording | 6.1 |
| CC7.4 | Containment runbook | 7.2 |
| CC8.1 | Team Setup change control, Auto-review, group overrides, inactive computers | 3.3, 4.1, 4.3, 7.3 |
| CC9.2 | Connector policy | 2.1 |
| C1.1 | Template sharing | 5.1 |
NIST 800-53 Rev 5 Mapping
| Control | Grok Bot Control | Guide Section |
|---|---|---|
| AC-2 | Access scoping, Grok Business seats, offboarding | 1.1, 1.3, 7.1 |
| AC-3 | Access scoping, Google approval, Auto-review, template sharing | 1.1, 2.2, 4.1, 5.1 |
| AC-4 | Network Controls, Allow Local Egress | 3.1, 3.2 |
| AC-6 | Connectors, Google approval, Cloud Agents, local execution, Auto-review, Team Rules, Team Bots | 2.1, 2.2, 2.3, 2.4, 4.1, 4.2, 5.2 |
| AC-17 | IdP exception, local execution, Allow Local Egress | 1.2, 2.4, 3.2 |
| AC-20 | IdP exception, Grok Business seats | 1.2, 1.3 |
| AC-21 | Template sharing, Team Bots | 5.1, 5.2 |
| AU-2 | Action Recording, control-plane audit | 6.1, 6.2 |
| AU-3 | Action Recording | 6.1 |
| AU-6 | Group override audit, control-plane audit | 4.3, 6.2 |
| AU-12 | Action Recording | 6.1 |
| CM-2 | Inactive computers | 7.3 |
| CM-3 | Team Setup, group overrides | 3.3, 4.3 |
| CM-5 | Team Setup | 3.3 |
| CM-6 | Group overrides | 4.3 |
| CM-7 | Connectors, Google approval, Cloud Agents, local execution | 2.1, 2.2, 2.3, 2.4 |
| IA-2 | IdP exception | 1.2 |
| IA-5 | Team Bots | 5.2 |
| IA-5(7) | Team Secrets | 3.3 |
| IR-4 | Containment runbook | 7.2 |
| IR-8 | Containment runbook | 7.2 |
| MP-6 | Offboarding data deletion | 7.1 |
| PL-4 | Team Rules | 4.2 |
| PS-4 | Offboarding | 7.1 |
| SA-9 | Connectors | 2.1 |
| SC-7 | Network Controls, Allow Local Egress | 3.1, 3.2 |
| SC-7(5) | Network Controls | 3.1 |
| SI-4 | Team Setup EDR, Auto-review, Action Recording, control-plane audit | 3.3, 4.1, 6.1, 6.2 |
ISO 27001:2022 Mapping
| Control | Grok Bot Control | Guide Section |
|---|---|---|
| A.5.10 | Team Rules | 4.2 |
| A.5.14 | Template sharing | 5.1 |
| A.5.15 | Access scoping, Google approval, Auto-review, Team Bots | 1.1, 2.2, 4.1, 5.2 |
| A.5.17 | IdP exception, Team Secrets, Team Bots | 1.2, 3.3, 5.2 |
| A.5.18 | Access scoping, Grok Business seats, offboarding | 1.1, 1.3, 7.1 |
| A.5.19 | Connectors | 2.1 |
| A.5.23 | Grok Business seats, connectors, Google approval | 1.3, 2.1, 2.2 |
| A.5.24 | Containment runbook | 7.2 |
| A.5.26 | Containment runbook | 7.2 |
| A.6.5 | Offboarding | 7.1 |
| A.8.1 | Local execution | 2.4 |
| A.8.5 | IdP exception | 1.2 |
| A.8.9 | Cloud Agents, local execution, Team Setup, Auto-review, group overrides, inactive computers | 2.3, 2.4, 3.3, 4.1, 4.3, 7.3 |
| A.8.10 | Offboarding data deletion | 7.1 |
| A.8.15 | Action Recording, control-plane audit | 6.1, 6.2 |
| A.8.16 | Action Recording, control-plane audit | 6.1, 6.2 |
| A.8.20 | Network Controls, Allow Local Egress | 3.1, 3.2 |
| A.8.22 | Network Controls, Allow Local Egress | 3.1, 3.2 |
| A.8.32 | Team Setup change control | 3.3 |
CIS Controls v8 Mapping
| Safeguard | Grok Bot Control | Guide Section |
|---|---|---|
| 2.5 | Connectors, Google approval | 2.1, 2.2 |
| 2.7 | Team Setup scripts | 3.3 |
| 3.3 | Template sharing, Team Bots | 5.1, 5.2 |
| 4.1 | Local execution, Team Setup, Auto-review, Team Rules, group overrides, inactive computers | 2.4, 3.3, 4.1, 4.2, 4.3, 7.3 |
| 4.8 | Connectors, Cloud Agents, local execution | 2.1, 2.3, 2.4 |
| 5.3 | Offboarding | 7.1 |
| 6.1 | Access scoping, Grok Business seats, Google approval | 1.1, 1.3, 2.2 |
| 6.2 | Grok Business seats, offboarding | 1.3, 7.1 |
| 6.3 | IdP exception | 1.2 |
| 6.7 | Access scoping | 1.1 |
| 6.8 | Team Bots | 5.2 |
| 8.2 | Action Recording, control-plane audit | 6.1, 6.2 |
| 8.5 | Action Recording | 6.1 |
| 8.9 | Action Recording, control-plane audit | 6.1, 6.2 |
| 8.11 | Group override audit, control-plane audit | 4.3, 6.2 |
| 9.3 | Network Controls | 3.1 |
| 10.1 | Team Setup EDR | 3.3 |
| 12.2 | Allow Local Egress | 3.2 |
| 13.4 | Network Controls, Allow Local Egress | 3.1, 3.2 |
| 17.4 | Containment runbook | 7.2 |
OWASP Top 10 for Agentic Applications (2026) and LLM Top 10 (2025)
| Risk | Grok Bot Control | Guide Section |
|---|---|---|
| ASI01 Agent Goal Hijack | Network Controls, Auto-review, Team Bots | 3.1, 4.1, 5.2 |
| ASI02 Tool Misuse | Connectors, Cloud Agents, local execution | 2.1, 2.3, 2.4 |
| ASI03 Identity & Privilege Abuse | Access scoping | 1.1 |
| ASI05 Unexpected Code Execution | Local execution | 2.4 |
| ASI06 Memory & Context Poisoning | Team Bots | 5.2 |
| ASI07 Insecure Inter-Agent Communication | Team Bots | 5.2 |
| ASI09 Human-Agent Trust Exploitation | Auto-review | 4.1 |
| ASI10 Rogue Agents | Action Recording | 6.1 |
| LLM06:2025 Excessive Agency | Auto-review | 4.1 |
OWASP risk names are taken from OWASP’s Agentic Top 10 launch post and LLM06:2025 Excessive Agency. They are framework mappings, not configuration requirements.
CISA SCuBA
| Policy | Relationship | Guide Section |
|---|---|---|
| MS.AAD.3.1v1 | Labeled compatibility exception: the vendor’s Bot-browser rule relaxes phishing-resistant MFA | 1.2 |
| MS.AAD.3.7v1 | Labeled compatibility exception: the vendor’s Bot-browser rule admits an unmanaged device | 1.2 |
Appendix A: Edition/Plan Availability
Self-serve Cursor Teams cannot turn Grok Bot off. It lacks Network Controls, Enforce Auto-review and Auto-review rules, Team Setup and Team Secrets, Allow Local Egress, Action Recording, audit logs, OpenTelemetry Export, SCIM, group Grok Bot tabs and Manage Bot Computers, so it also has no data-deletion path.
A Teams admin can apply:
- 1.1 through team membership and IdP assignment
- 1.2 and 2.2 on the IdP and Google side
- 2.1, 2.3, 2.4, 4.2, 5.1 and the Manage Team Bots part of 5.2
- parts of 7.1 and 7.2
- the Admin API reads documented as available on every plan
Whether Teams keys can call the PATCH and POST routes is unconfirmed (8.6).
Individual accounts, linked SuperGrok and X Premium+ accounts and Grok Business seats have member settings only (8.7), apart from Grok Business seat assignment (1.3) and Google-side app approval (2.2).
| Control | Cursor Enterprise | Cursor Teams (self-serve) | Individual, linked or Grok Business seat |
|---|---|---|---|
| 1.1 Limit to approved groups | ✅ Enable switch and Manage Group Access | Partial: team membership and IdP assignment only | ❌ |
| 1.2 IdP sign-in exception | ✅ (IdP-side; Entra passkey strength needs Team Setup) | ✅ (IdP-side) | ❌ |
| 1.3 Grok Business seats | n/a | n/a | ✅ console.x.ai seat assignment (self-serve Grok Business only) |
| 2.1 Connector policy | ✅ Team Marketplaces, MCP allowlist, audit pack | ✅ Team Marketplaces (no audit logs) | ❌ |
| 2.2 Google approval of Grok | ✅ (Google-side) | ✅ (Google-side) | ✅ (Google-side, for company Google accounts) |
| 2.3 Cloud Agents | ✅ | ✅ (API write unconfirmed) | ❌ |
| 2.4 Local execution ceiling | ✅ | ✅ (API write unconfirmed) | ❌ (member setting only) |
| 3.1 Network Controls | ✅ | ❌ | ❌ |
| 3.2 Allow Local Egress | ✅ | ❌ | ❌ |
| 3.3 Team Setup and Team Secrets | ✅ | ❌ | ❌ |
| 4.1 Enforce Auto-review | ✅ | ❌ | ❌ (personal rules only) |
| 4.2 Team Rules | ✅ | ✅ (API write unconfirmed) | ❌ |
| 4.3 Group Grok Bot tabs | ✅ | ❌ (no group tabs) | ❌ |
| 5.1 Public template sharing | ✅ (starts off) | ✅ (starts allowed; API write unconfirmed) | ❌ |
| 5.2 Team Bots (public beta) | ✅ Manage Team Bots and audit pack | Partial: Manage Team Bots only | ❌ |
| 6.1 Action Recording and OpenTelemetry | ✅ | ❌ | ❌ |
| 6.2 Audit logs | ✅ | ❌ | ❌ |
| 7.1 Offboarding | ✅ Delete VMs and Data, team removal, IdP revocation | Partial: team removal and IdP revocation; no data deletion | ❌ (account deletion under Cursor terms) |
| 7.2 Containment | ✅ Disable, Manage Group Access, Terminate | Partial: member removal, connector block, local-execution and Cloud Agent switches | ❌ |
| 7.3 Terminate Inactive Computers | ✅ (organization admins) | ❌ | ❌ |
Admin API reads (/access, /network, /auto-review) |
✅ | ✅ (documented as every plan) | ❌ |
| Admin API writes | ✅ | Unconfirmed | ❌ |
| Organization API computer operations | ✅ (turned on per team by the account team) | ❌ | ❌ |
| SCIM | ✅ | ❌ | ❌ |
Grok Enterprise has no documented Grok Bot controls; access goes through the account manager.
Appendix B: References
xAI Grok Bot documentation (docs.x.ai):
- Grok Bot for teams and enterprises: availability, every admin control and the recommended baseline
- Grok Bot security
- Grok Bot security FAQ
- Team Bots
- Configure identity and access
- Manage Grok Bot computers
- Connect to private networks
- Settings and notifications
- Approvals, security, and privacy
- Tag @bot on X
- Create and manage Bots
- Computer and apps
- Skills, routines, and automations
xAI announcements and release notes:
- Introducing Grok Bot (2026-08-11)
- Grok Bot is now included with more plans (2026-08-26)
- Grok Bot for Enterprise (2026-09-03)
- Team Bots (2026-09-28)
- Grok Bot changelog: returns a Cloudflare 403 to automated fetchers; read in a real browser on 2026-10-08 and re-read on 2026-10-09. Every changelog entry this guide cites (V0.30.0, V0.33.0, V0.37.0 of September 3, 2026, V0.53.0, V0.57.0, V0.64.0, V0.67.0, V0.68.0) rests on those reads
xAI Grok Business and Management API:
- Grok Business management (console.x.ai)
- Management API reference
- Management API: billing
- Management API: audit
Cursor documentation (the Grok Bot admin plane):
- Cursor: Grok Bot for teams
- Cursor: Grok Bot security
- Cursor: Grok Bot deployment: MDM reaches only the apps; the hosted computer is not MDM-enrolled
- Cursor: Grok Bot settings
- Cursor: Manage Grok Bot computers
- API overview
- Admin API: Grok Bot
- Admin API: audit logs
- Admin API (remove-member, members, billing groups, model-access configuration)
- Billing groups
- Organization API: Grok Bot computers
- Compliance and monitoring (audit logs)
- OpenTelemetry Export
- OpenTelemetry Export: wire reference
- Model and integration management
- Plugins and Team Marketplaces
- Plugins reference: marketplace manifest
- SCIM
- SSO
- Identity and access management
Cursor help center:
- Plans and billing
- SuperGrok and Grok Bot
- Connect plugins
- Team Bots
- Group chats and Bot-to-Bot messages
- Routines
- Models
- Sign-in domains
Automation surface census (all fetched 2026-10-08):
- Terraform registry: cursor/cursor: 0.8.0 has only
origin_*andplatform_workflowresources and data sources, nothing for Grok Bot - Terraform registry: xai namespace: zero providers (the spacexai namespace is also empty)
- okta/okta 7.0.0: okta_app_signon_policy_rule, okta_app_signon_policy data source, okta_app_sign_on_policy_rule data source and okta_everyone_group data source, used by the 1.2 pack
- Terraform: checks (“Terraform reports a warning and continues”) and custom conditions (a failed postcondition stops the operation), which decide how the 1.2 read-back fails
- Okta Management API spec (2026.09.2): OAuth scopes and the
PolicyPlatformOperatingSystemTypeenum - hashicorp/azuread 3.10.0: azuread_conditional_access_policy, used by the 1.2 pack
- hashicorp/googleworkspace: no third-party app-access resource
- xAI CLI reference and Grok Build overview: the
grokCLI is the Grok Build coding agent, with no Grok Bot or org-admin subcommands - Cursor CLI overview: no Grok Bot verbs
- Cursor TypeScript SDK: no Grok Bot surface, and “Team Admin API keys are not yet supported”
- xai-sdk-python: no license-assignment or seat-management module
- xAI Organization (console.x.ai): SCIM role-to-license provisioning, “exclusive to the Enterprise tier”
Identity provider and Google Workspace documentation:
- Okta: add an app sign-in policy rule
- Microsoft: Conditional Access conditions
- Microsoft: Building a Conditional Access policy
- Microsoft: Conditional Access users and groups
- Microsoft: Conditional Access target resources
- Microsoft: assign users and groups to an enterprise app
- Microsoft: require MFA strength for all users
- Google: control which third-party and internal apps access Google Workspace data
- Google: review and manage third-party app access requests
- Google: add and configure third-party apps in bulk
- Cloud Identity Policy API: supported settings
Benchmarks and frameworks:
- CISA ScubaGear Entra ID (AAD) baseline
- OWASP Top 10 for Agentic Applications (launch post)
- OWASP LLM06:2025 Excessive Agency
Security research and incidents:
- The lethal trifecta, Simon Willison (2025-06-16)
- CVE-2025-54135 CNA record (CVE Program API)
- Adversa AI: Cryptographic Context Injection against Grok (2026-08-20)
- Radware: ShadowLeak (2025-09-18): bot wall to automated fetchers; read in a real browser on 2026-10-08
- Radware: ZombieAgent (2026-01-08): read in a real browser on 2026-10-08
- NeuralTrust: Grok chatbot and Bankrbot Morse-code transfer (2026-05-08)
- Brave: research PoC of agentic browser prompt injection in Perplexity Comet (2025-08-20)
- DISA STIG Document Library: JavaScript-rendered; searched in a real browser on 2026-10-09
- PromptArmor: data exfiltration from Slack AI (August 2024)
- KrebsOnSecurity: xAI dev leaks API key (2025-05-01)
Related How to Harden guides:
- Cursor: org-wide SSO, SCIM, Privacy Mode, MCP allowlist, Cloud Agents, network allowlist, audit export and spend
- ChatGPT Enterprise, section 6: the workspace-agent hardening pattern and the lethal-trifecta framing
- Okta, Microsoft Entra ID, Google Workspace, Slack
Changelog
| Date | Version | Maturity | Changes | Author |
|---|---|---|---|---|
| 2026-10-09 | 0.1.0 | ai-drafted | Initial guide: 20 controls in 7 sections (access and identity, agent reach and connected apps, hosted computer and network, autonomy and approvals, sharing and Team Bots, monitoring and audit, lifecycle and containment), plus an unleveled Known Gaps and Member Guidance section, Compliance Quick Reference, plan availability and references. 17 controls carry Code Pack includes (16 api and 1 terraform, plus 13 Sigma rules); 1.3, 2.2 and 7.3 carry evidenced ClickOps-only verdicts, and 2.1, 4.3, 5.2 and 6.2 enforce through ClickOps with read-only packs. Drafted from vendor documentation fetched 2026-10-08 and corrected after an independent review against pages re-fetched 2026-10-09; no tenant was observed | Claude Code (Opus 5.5) |
Contributing
Found an issue or want to improve this guide?
- Report outdated information: Open an issue with tag
content-outdated - Propose new controls: Open an issue with tag
new-control - Submit improvements: See Contributing Guide