Skip to content
All pages

Integrations

Connect GitHub, Slack, Stripe and seven other services so agents in a space can call them through the gateway without ever holding the real key.

moatbox runs coding agents (Claude Code, Codex, opencode) inside spaces: Firecracker microVMs with their own disk. A gateway inside each space sits between the agent and the network. An integration teaches that gateway one outside service: which hosts it lives on, which credential it needs, and which calls are risky enough to wait for a person.

This page covers how integrations work in general, how you connect one, and what each of the ten built-in integrations holds by default. The hold strategies (pass, approve-only, compensate, checkpoint, hold) are defined in Approvals.

How an integration works

  1. You paste a key for the service on the Integrations page of the dashboard. moatbox encrypts it and shows only its last 4 characters from then on.
  2. moatbox delivers the key to the gateway of every space it applies to. The agent never receives it.
  3. The agent gets a placeholder instead, in the environment variable the service's tools expect (for example STRIPE_API_KEY=moatbox-ph-<32 hex characters>).
  4. The space trusts a moatbox certificate authority, so the gateway can read HTTPS requests to the integration's hosts. When a request leaves for one of those hosts, the gateway swaps the placeholder for the real credential. It does this at send time, so a request that waited for approval still goes out with a current token.
  5. Before it sends, the gateway classifies the request. Risky calls wait for approval, some of them with an undo recorded. Calls the integration does not list go through.

If the placeholder shows up in a request to any other host the gateway can read, the gateway refuses it with 403 moatbox: credential sent to wrong host and logs a credential.wrong-host event.

When a space's network is restricted, a connected integration's hosts are allowed for as long as the integration stays connected. The space's Network page lists them under Connected integrations.

Connect an integration

  1. Open Integrations in the dashboard sidebar. Services you have not connected appear under Connect a service (or Available once you have connected one).
  2. Click a service. Most open a guided form: step 1 is a button such as Open GitHub that opens the service with the key's name and permissions filled in, step 2 is where you paste the key.
  3. Under Saved as, rename the key if you like and pick which spaces get it: every space or only <space>.
  4. Click Save and test.

The dashboard checks the key's shape before it saves. It recognizes token types by their prefix and refuses the types that reach more than an agent needs or expire within hours, and says why: a classic GitHub token (ghp_…), a Slack user token (xoxp-…), a Cloudflare Global API Key, a Gmail access token (ya29.…).

After saving, the dashboard reports Delivered to every running space, or names the spaces it has not reached yet and retries every few seconds.

Each saved key has three buttons:

  • Test asks a space to call the service with the credential that space uses, and shows who the service says you are (for GitHub: the login, how many repositories the token reaches, and when it expires). If the space has its own environment variable or OAuth file for the credential, the test uses that and the result says it overrides the key. A sleeping space wakes first. The test gives up after 20 seconds.
  • Rename changes the key's label.
  • Remove takes the key out of every space it reached. The key itself keeps working in the service until you delete it there.

Who can do what:

  • Owners and admins set keys for every space. Members can set a key for a single space.
  • A key set for one space replaces the team key in that space.
  • An environment can limit which integrations reach its spaces, or turn them all off. See Environments.
  • CI spaces receive no integration keys. See GitHub Actions runners.

The form warns: root in a Full machine space can read the key.

Set a key through the API

The dashboard calls the same endpoint you can call with an API key:

curl -X PUT https://app.moatbox.dev/api/v1/teams/<team-id>/integrations/stripe \
  -H "Authorization: Bearer mbx_xxx" \
  -H "Content-Type: application/json" \
  -d '{"value": "<your-restricted-key>", "space": "my-space", "name": "moatbox my-space"}'

Leave space empty to set the key for every space. AWS and Gmail take named fields instead of value (listed in their sections below). The answer names the key's kind, its last 4 characters, and pending, the spaces that have not received it yet. DELETE on the same path removes a key (add ?space=<slug> for a space's own key), and POST /api/v1/spaces/<space>/integrations/<name>/test runs the test.

An API key without the environment.write permission can only set a key for one named space. See API keys.

Which processes get the placeholder

  • GitHub is signed in for every shell in a space that has a GitHub key: gh and git work through the gateway in each new terminal.
  • Any integration reaches an agent run when the run lists it in broker:
curl -X POST https://app.moatbox.dev/api/v1/spaces/my-space/runs \
  -H "Authorization: Bearer mbx_xxx" \
  -H "Content-Type: application/json" \
  -d '{"agent": "claude", "task": "Refund the duplicate charge on the Acme invoice", "broker": ["stripe", "github"]}'

A run that names an integration with no key set fails to start, with an error naming the missing credential.

The dashboard's start form sends no broker list, so runs started from the dashboard get no integration credentials. Start the run through the API with broker when the agent needs one.

Rules: what gets held

The Default rule section of each integration's dialog sets how strict the gateway is. You can also give a single key its own rule.

  • Hold risky actions (the default) holds the actions listed for each integration below.
  • Hold every action holds every action the integration matches, including the ones it would let through, and every other request except GET, HEAD and OPTIONS. Actions that would send after a window wait for a person instead.
  • Auto-approve listed actions lets the action kinds you list (such as slack.post) go through without asking, when their strategy is hold or approve-only. Listed kinds with checkpoint or compensate still wait. Everything else is held as under Hold risky actions.

Members can make a rule stricter. Only owners and admins can loosen one. Approvals covers approving, denying and undoing.

Limits that apply to every integration

On an integration's hosts the gateway refuses:

  • a request carrying a method override header or parameter (X-HTTP-Method-Override, _method and similar),
  • a protocol upgrade, such as a WebSocket,
  • a path with . or .. segments, or empty segments outside an S3 key.

A request body over 1 MiB is too large for the gateway to read. It is held for approval as <integration>.uninspected unless the matched action already records an undo or waits for a person.

Integrations

Each section lists the hosts where the gateway swaps the credential, how to connect, the variable that holds the placeholder, and what Hold risky actions does. The Kind column is the name shown in approvals and the audit log.

"Recorded" means the call goes through and leaves an entry in the audit log. Calls not listed go through without an entry.

GitHub

  • Hosts: github.com, api.github.com
  • Placeholder: GITHUB_TOKEN and GH_TOKEN. git is configured to send git@github.com: remotes over HTTPS and to use the placeholder as its password.
  • Connect: Open GitHub creates a fine-grained token (github_pat_…) with a 90-day expiry and write access to contents, pull requests and issues. You pick the owner and the repositories. Or run moatbox github connect <space> on your laptop: it stores the token gh is signed in with (gho_…) as that space's key and tests it.
Action Kind Strategy Undo
git push that rewrites a branch (force push) github.force-push compensate Pushes the old commit back. GitHub refuses if anyone pushed since.
git push that deletes a branch or tag github.push-delete compensate Recreates it at the old commit.
git push moatbox cannot read github.push approve-only No
Force update of a ref through the REST API github.ref-force-update compensate Pushes the old commit back.
Delete a ref through the REST API github.ref-delete approve-only No
Open a pull request moatbox cannot read github.pr-create approve-only No
Delete a repository github.repo-delete approve-only No
Change repository settings, deploy keys, collaborators, webhooks, invitations, Actions runners, or transfer the repository github.access approve-only No
Change organization webhooks, members, invitations, teams or runners, team endpoints, or add an SSH key to the account github.access approve-only No
GraphQL deleteRef, updateRef, updateRefs github.graphql-ref approve-only No
Any other GraphQL mutation, or one moatbox cannot read github.graphql approve-only No
Any other REST write github.write approve-only No

Recorded: pushes that create refs or fast-forward, ref updates without force, opening pull requests and issues, updating or labelling issues and pull requests, comments and reviews, and the GraphQL mutations createPullRequest, createIssue, addComment, addPullRequestReview, addPullRequestReviewComment, addPullRequestReviewThread and submitPullRequestReview.

git does not tell the server a push was forced. The gateway reads the pushed commits and treats a push as a fast-forward only when it can trace the new commit back to the old one. When it cannot, it holds the push.

Slack

  • Hosts: slack.com
  • Placeholder: SLACK_TOKEN
  • Connect: Open Slack creates a Slack app with its name and permissions filled in. Install it to the workspace, paste the Bot User OAuth Token (xoxb-…), and invite the bot to each channel with /invite. User, app-level, rotating and browser tokens are refused.
Action Kind Strategy Undo
chat.postMessage, chat.meMessage slack.post hold, 30 s window No
chat.update slack.update hold, 30 s window No
chat.scheduleMessage slack.schedule hold, 30 s window No
chat.postEphemeral slack.ephemeral hold, 30 s window No
files.upload, files.completeUploadExternal, files.share slack.file-share hold, 30 s window No
chat.delete slack.delete approve-only No
conversations.archive slack.archive approve-only No
conversations.kick slack.kick approve-only No
files.delete slack.file-delete approve-only No
Any admin.* method slack.admin approve-only No
Any method moatbox does not know, or arguments it cannot read slack.call approve-only No

A 30-second window means the message sends after 30 seconds unless someone cancels it. Approving sends it at once.

Recorded as slack.call: search.*, methods whose last part starts with get or is list, info, history, replies, members, test or lookupByEmail, and reactions.add, reactions.remove, conversations.open, conversations.join, conversations.mark.

Discord

  • Hosts: discord.com
  • Placeholder: DISCORD_BOT_TOKEN
  • Connect: Open Discord opens the Developer Portal. Create an application, turn Public Bot off, turn on Message Content Intent if the agent should read messages, click Reset Token, and paste the bot token. Step 3, Invite the bot, adds it to your server with permissions to view channels, read history, send messages, embed links and attach files.
Action Kind Strategy Undo
Post a message discord.message-create compensate Deletes the message.
Post a message moatbox cannot read discord.message-create approve-only No
Edit a message's content, embeds, components or flags discord.message-edit compensate Restores the old values. Refused if the message changed since.
Edit with attachments or components v2, or an edit moatbox cannot read discord.message-edit approve-only No
Delete a message discord.message-delete approve-only No
Bulk-delete messages discord.message-bulk-delete approve-only No
Delete a channel discord.channel-delete approve-only No
Change a channel's permission overwrites discord.channel-permissions approve-only No
Any webhook endpoint discord.webhook approve-only No
Delete the server discord.guild-delete approve-only No
Ban or bulk-ban discord.ban approve-only No
Kick a member or prune members discord.kick approve-only No
Add, remove or set a member's roles discord.member-roles approve-only No
Create, reorder, modify or delete roles discord.role-change approve-only No

Recorded: message edits that touch none of content, embeds, components or flags.

Gmail

  • Hosts: gmail.googleapis.com
  • Placeholder: GMAIL_ACCESS_TOKEN. The gateway trades your refresh token for short-lived access tokens. The agent sees neither.
  • Connect: You bring your own Google OAuth client. Open Google Cloud, enable the Gmail API, add yourself as a test user, and create a Web application client with https://developers.google.com/oauthplayground as a redirect URI. In the OAuth Playground, use your own client, authorize https://www.googleapis.com/auth/gmail.modify, exchange the code, and paste the client ID, client secret and refresh token. While the app's consent screen has the External user type and the Testing publishing status, Google issues refresh tokens that expire after 7 days. Move the app to In production before you create the refresh token to avoid that limit.
  • API fields: client_id, client_secret, refresh_token.
Action Kind Strategy Undo
Send a message gmail.send hold, no window No
Send a draft gmail.draft-send hold, no window No
Move a message or thread to the trash gmail.trash, gmail.thread-trash compensate Takes it out of the trash.
Permanently delete a message or thread gmail.delete, gmail.thread-delete approve-only No
Delete a draft gmail.draft-delete approve-only No
messages/batchDelete gmail.batch-delete approve-only No
Create or delete a mail filter gmail.filter-create, gmail.filter-delete approve-only No
Add or remove a forwarding address gmail.forwarding-create, gmail.forwarding-delete approve-only No
Change auto-forwarding gmail.auto-forwarding approve-only No
A batch request gmail.batch approve-only No
Any other delete gmail.delete approve-only No

A hold with no window waits for a person, as approve-only does.

Stripe

  • Hosts: api.stripe.com
  • Placeholder: STRIPE_API_KEY
  • Connect: Pick Test or Live, then Open Stripe creates a restricted key (rk_…) with its permissions filled in. It can manage customers, products, prices, coupons, promotion codes, payment links, checkout, the customer portal, invoices, subscriptions and webhooks, take payments and refunds, and read the balance, payouts, transfers, disputes and tax rates. A full secret key (sk_…) works, with a warning. A publishable key (pk_…) is refused.
Action Kind Strategy Undo
Any DELETE stripe.delete approve-only No
Create a charge stripe.charge approve-only No
Capture a charge stripe.charge-capture approve-only No
Create a payment intent with confirm=true, or confirm one stripe.payment-intent-confirm approve-only No
Capture a payment intent stripe.payment-intent-capture approve-only No
Refund stripe.refund approve-only No
Payout stripe.payout approve-only No
Transfer, or reverse a transfer stripe.transfer, stripe.transfer-reversal approve-only No
Top up the balance stripe.topup approve-only No
Pay an invoice, or mark it paid outside Stripe stripe.invoice-pay approve-only No
Create or update a subscription stripe.subscription, stripe.subscription-update approve-only No
Issue a credit note stripe.credit-note approve-only No
Update a customer, product or price stripe.customer-update, stripe.product-update, stripe.price-update compensate Restores the old values. Refused if a field the update touched changed since.
Update a customer that sets a card, bank account or source stripe.customer-update approve-only No
Any other POST stripe.write approve-only No

These creates go through: customers, products, prices, coupons, promotion codes, checkout sessions, customer portal sessions, payment links, setup intents, and payment intents without confirm=true.

Cloudflare

  • Hosts: api.cloudflare.com
  • Placeholder: CLOUDFLARE_API_TOKEN
  • Connect: Open Cloudflare starts an account API token with its name and permissions filled in. Limit the zone resources to the zones the agent should touch, create the token (cfat_…), and paste it. User tokens (cfut_…) and older 40-character tokens also work. The Global API Key, Origin CA keys and Wrangler OAuth tokens are refused.
Action Kind Strategy Undo
Delete a zone cloudflare.zone-delete approve-only No
Delete a zone setting cloudflare.zone-setting-delete approve-only No
Batch or import DNS records cloudflare.dns-batch, cloudflare.dns-import approve-only No
Update a DNS record cloudflare.dns-update compensate Restores the old record. Refused if the record changed since.
Delete a DNS record cloudflare.dns-delete compensate Recreates the record.
Delete an R2 bucket cloudflare.r2-bucket-delete approve-only No
Upload a Worker cloudflare.worker-upload compensate Redeploys the previous versions, or deletes a Worker that did not exist before. Refused if the Worker was deployed again since.
Change a Worker's deployment cloudflare.worker-deploy compensate Redeploys the previous versions.
Delete a Worker cloudflare.worker-delete approve-only No
Any other DELETE cloudflare.delete approve-only No

A DNS update the gateway cannot read is held as approve-only.

Hetzner

  • Hosts: api.hetzner.cloud
  • Placeholder: HCLOUD_TOKEN
  • Connect: Open Hetzner opens the Hetzner Console. A token reaches one whole project, so use a project made for the agent. Under Security, API tokens, generate a Read & Write token and paste it (64 letters and digits). Tokens from the old DNS Console are refused.
Action Kind Strategy Undo
Create a server hetzner.server-create compensate Deletes the server.
Delete a server hetzner.server-delete approve-only No
Rebuild a server (wipes its disk) hetzner.server-rebuild approve-only No
Any other write hetzner.write approve-only No

A server create the gateway cannot read is held as approve-only.

Linear

  • Hosts: api.linear.app
  • Placeholder: LINEAR_API_KEY
  • Connect: Open Linear opens Security & access. Create a personal API key (lin_api_…) with only the permissions the agent needs and limit it to its teams. The key acts as you, so its changes show your name. OAuth tokens (lin_oauth_…) are refused.
Action Kind Strategy Undo
issueUpdate that changes the state linear.issue-state compensate Restores the old values. Refused if a changed field moved since.
issueUpdate of title, description, priority, estimate, due date, labels, assignee, project, cycle, parent, team and similar fields linear.issue-update checkpoint Restores the old values. Refused if a changed field moved since.
issueUpdate with a field moatbox cannot restore, or one it cannot read linear.issue-update approve-only No
issueDelete linear.issue-delete approve-only No
projectDelete linear.project-delete approve-only No
issueBatchUpdate linear.issue-batch-update approve-only No
Mutations starting with team linear.team-change approve-only No
Mutations starting with workflowState linear.workflow-change approve-only No
Any other mutation ending in Delete or Archive linear.delete approve-only No
Several mutations in one request, if any of them would be held linear.mutation approve-only No
A mutation moatbox cannot read linear.mutation approve-only No

Recorded as linear.mutation: every other mutation, such as creating issues and comments.

Notion

  • Hosts: api.notion.com
  • Placeholder: NOTION_TOKEN
  • Connect: Open Notion opens your connections (a workspace owner has to do this). Create an internal connection, keep the content and comment capabilities, add only the pages the agent may use under Content access, and paste the Internal Integration Secret (ntn_…, or secret_… for older ones). The connection sees only pages you share with it.
Action Kind Strategy Undo
Update a page or block notion.page-update, notion.block-update checkpoint Writes back the old values.
Move a page notion.page-move checkpoint Moves it back. A page whose old parent is not a page or data source can't be moved back, so the approved move fails and the page stays put.
Append blocks notion.block-append compensate Deletes the added blocks.
Delete a block notion.block-delete compensate Restores it from the trash.
Move a page, block, database or data source to the trash notion.page-trash, notion.block-trash, notion.database-trash, notion.data-source-trash compensate Restores it from the trash.
Update a page with erase_content or a template notion.page-content approve-only No
Rewrite a page from markdown notion.page-markdown approve-only No
Update a database or data source notion.database-update, notion.data-source-update approve-only No
Delete a comment or view notion.comment-delete, notion.view-delete approve-only No
An update moatbox cannot read notion.<object>-update approve-only No

Recorded: creating pages, adding comments, and restoring from the trash.

AWS

  • Hosts: s3.amazonaws.com, ec2.amazonaws.com, iam.amazonaws.com, sts.amazonaws.com, plus the regional S3, EC2, RDS and STS endpoints (s3.<region>.amazonaws.com and so on) in each region the dashboard offers.
  • Placeholder: AWS_ACCESS_KEY_ID=AKIAMOATBOXEXAMPLE00 and a placeholder AWS_SECRET_ACCESS_KEY. AWS_REGION and AWS_DEFAULT_REGION hold the region you picked. The agent's SDK signs with the placeholder, and the gateway signs each request again with your key. It re-signs only header-signed SigV4 requests and refuses chunked (streaming) S3 uploads.
  • Connect: Open AWS opens IAM on Create user. Create a user with only the policies the agent needs, create an access key for it (Third-party service), and paste the access key ID, secret access key and region. A temporary key (ASIA…) also needs its session token.
  • API fields: access_key_id, secret_access_key, region, and session_token for a temporary key.
Action Kind Strategy Undo
Delete an S3 object aws.s3-delete-object compensate Removes the delete marker. Works only when the bucket has versioning on.
Delete several S3 objects (DeleteObjects) aws.s3-delete-objects compensate Same as above. Held as approve-only when the request names versions or uses quiet mode.
Delete a specific object version aws.s3-delete-version approve-only No
Delete a bucket aws.s3-delete-bucket approve-only No
Any other S3 delete aws.s3-delete approve-only No
Any other S3 write, including uploads that set ACL headers aws.s3-write approve-only No
Any IAM write aws.iam approve-only No
TerminateInstances aws.ec2-terminate approve-only No
Other EC2 or RDS actions starting with Delete or Terminate aws.ec2-delete, aws.rds-delete approve-only No
DeleteDBInstance with a final snapshot aws.rds-delete-instance compensate Restores the instance from that snapshot.
DeleteDBInstance without a final snapshot aws.rds-delete-instance approve-only No
Any other EC2 or RDS write aws.ec2, aws.rds approve-only No

Recorded: StartInstances, StopInstances, RebootInstances, CreateTags, CreateDBSnapshot. S3 object uploads without ACL headers, read actions (Describe*, Get*, List*, Simulate*, Generate*) and STS calls go through, as does aborting a multipart upload (DELETE with uploadId).