Skip to main content
There are four patterns teams usually reach for when giving agents API access. Each solves the immediate problem quickly, but each leaves security or operability gaps.

Hardcoded API keys

The simplest pattern: read one token from an environment variable and use it for every request.
import { Octokit } from "@octokit/rest";

const octokit = new Octokit({
  auth: process.env.GITHUB_TOKEN,
});

// Every agent instance, every user, every task: same token
const repos = await octokit.repos.listForAuthenticatedUser();
A common MCP server pattern looks like this:
# mcp_server.py
import os
from github import Github

GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]  # read once at startup

@server.tool("list_issues")
async def list_issues(repo: str) -> str:
    gh = Github(GITHUB_TOKEN)               # same token for every caller
    issues = gh.get_repo(repo).get_issues(state="open")
    return "\n".join([f"#{i.number}: {i.title}" for i in issues])

@server.tool("create_issue")
async def create_issue(repo: str, title: str, body: str) -> str:
    gh = Github(GITHUB_TOKEN)               # same token, now used for write
    issue = gh.get_repo(repo).create_issue(title=title, body=body)
    return f"Created issue #{issue.number}"
It works for demos. In production, every caller gets the same long-lived power.

Shared service account

A step up from a raw key: create one bot account per platform and share it across agents, scripts, and pipelines.
# config.yaml shared across multiple services
github:
  token: ghp_xxxxxxxxxxxxxxxxxxxx  # github-bot
slack:
  token: xoxb-xxxxxxxxxxxx         # eng-bot
linear:
  api_key: lin_xxxxxxxxxxxx        # automation account
const config = readConfig("config.yaml");
const octokit = new Octokit({ auth: config.github.token });

await octokit.pulls.create({
  owner: "acme-corp",
  repo: "backend",
  title: "Fix: update dependency",
  head: "fix/dep-update",
  base: "main",
});
Audit logs show one actor (github-bot) for many systems. Attribution becomes guesswork.

User token passthrough

Here, the agent receives the user’s OAuth token directly.
async function handleAgentRequest(userSession: UserSession) {
  const agent = new Agent({
    githubToken: userSession.accessToken,
    slackToken: userSession.slackAccessToken,
  });

  await agent.run(userSession.prompt);
}
This feels natural but usually overgrants permissions.
What task needs:                  What token grants:
- Read PRs in acme/backend        - Read PRs in acme/backend
                                  - Read/write across other repos
                                  - Admin capabilities the user has
                                  - Broader org/workspace access
The agent inherits the user’s full authority, not the task’s minimum authority.

Long-lived personal access tokens

Common in CLI agents and unattended automations: users paste PATs/API keys into .env.
# .env
GITHUB_PAT=ghp_xxxxxxxxxxxxxxxxxxxx
LINEAR_API_KEY=lin_api_xxxxxxxxxxxx
SLACK_TOKEN=xoxb-xxxxxxxxxxxx
import os
from dotenv import load_dotenv

load_dotenv()

class AgentTools:
    def __init__(self):
        self.github_token = os.environ["GITHUB_PAT"]
        self.linear_key = os.environ["LINEAR_API_KEY"]
        self.slack_token = os.environ["SLACK_TOKEN"]

    async def create_pr(self, repo, title, branch):
        headers = {"Authorization": f"Bearer {self.github_token}"}
        async with httpx.AsyncClient() as client:
            resp = await client.post(
                f"https://api.github.com/repos/{repo}/pulls",
                headers=headers,
                json={"title": title, "head": branch, "base": "main"},
            )
        return resp.json()
Over time, the same token spreads widely:
grep -r "ghp_" ~/.config/ ~/projects/ ~/.env*
These tokens are broad, long-lived, and hard to rotate without outages.
These patterns ship quickly. They fail when agents move into production and teams need least privilege, attribution, and controlled revocation.