AI Codex
Building Your Internal AI StackStep 6 of 8
← Prev·Next →
Infrastructure & DeploymentHow It Works

Access Control for AI Agents — Why "Everyone Gets Everything" Breaks at Scale

In brief

When you give AI access to company data, tool-level, group-level, and user-level access control needs to be built into the architecture — not enforced by prompt. The difference: a sales rep's Claude never even sees payroll tools. They don't exist in their tool list.

9 min read·

Contents

Sign in to save

When you first give your team access to Claude connected to company data, the easy path is everyone gets everything. One configuration, all tools available to all users. Simple to set up.

This breaks in two ways. Fast.

The first way is the obvious one: someone accesses something they shouldn't. A sales rep pulls payroll data because they were curious. An intern sees customer billing details. It's usually not malicious — people just use what's in front of them. But the exposure is real.

The second way is subtler: the AI stops feeling like a tool and starts feeling like a liability. When executives realize that junior employees can ask Claude to pull org chart changes, executive comp surveys, or financial projections — the enterprise AI program gets a pause meeting. Not because of what happened, but because of what could happen. And you lose months of momentum.

Access control isn't about distrust. It's about making the AI feel appropriately shaped to each person's role — so it stays trusted.


The three layers you need

Solid AI access control has three layers:

Tool-level permissions. Each tool has an enable/disable toggle and a required permission level. The payroll query tool requires the hr_admin permission. The billing lookup requires finance_read. A sales rep's tool list doesn't include these tools — not because they're blocked at runtime, but because they're never offered.

Group-level assignment. Users are assigned to permission groups: sales, finance, cs, engineering, executive. Each group has a set of tools it can use. An executive group might get read access to everything. A CS group gets customer profiles and support tools. Sales gets CRM and call data.

User-level overrides. Occasionally, a specific person needs access that doesn't fit neatly into their group. A sales engineer who needs to see certain technical docs. A manager who needs one-time access to a report. The system supports per-user exceptions without having to create new groups.


How the ACL middleware works

The ACL middleware is the enforcement layer. It sits between Claude and every tool call.

// acl-middleware.ts

interface CallerContext {
  user_id: string
  org_id: string
  permission_group: string
  permissions: string[]  // e.g., ['crm_read', 'calls_read', 'tickets_read']
}

async function handleToolCall(
  toolName: string,
  params: Record<string, unknown>,
  caller: CallerContext
): Promise<ToolResult> {

  const toolConfig = TOOL_REGISTRY[toolName]

  // Tool doesn't exist
  if (!toolConfig) {
    return { error: 'Tool not found', code: 404 }
  }

  // Check permission
  const hasPermission = toolConfig.requiredPermissions.every(
    p => caller.permissions.includes(p)
  )

  if (!hasPermission) {
    // Log the attempt (useful for auditing)
    await logToolCall({
      user_id: caller.user_id,
      tool: toolName,
      status: 'denied',
      timestamp: new Date().toISOString(),
    })
    // Return a clean denial — don't leak what the tool does
    return { error: 'Not authorized', code: 403 }
  }

  // Inject caller identity into every call — tools never trust user-provided IDs
  const enrichedParams = {
    ...params,
    _caller_user_id: caller.user_id,
    _caller_org_id: caller.org_id,
  }

  // Execute the tool
  const result = await executeToolImpl(toolName, enrichedParams)

  // Log the successful call
  await logToolCall({
    user_id: caller.user_id,
    tool: toolName,
    status: 'success',
    timestamp: new Date().toISOString(),
  })

  return result
}

The key move in the middleware: caller identity (user_id, org_id) is injected by the server, not provided by the user or the prompt. A user cannot claim to be someone else to get different data. The identity comes from the authenticated session — it's a fact, not a parameter.


What tools each role sees

The principle is that unauthorized tools don't appear in Claude's tool list at all. This isn't just a UX nicety — it prevents Claude from offering to do things it can't actually do, which would be confusing and erode trust.

Here's a rough mapping of who sees what:

Tool Sales Finance CS Engineering HR Executive
get_deal_history
get_call_transcript
get_billing_status
get_ar_aging_report
get_employee_compensation
get_deploy_status
run_sql_query
get_customer_profile
get_roadmap

Sales sees deal history, call transcripts, customer profiles. Finance sees billing, AR, and SQL access. CS sees customer profiles and billing status. Engineering sees deploy status, roadmap, and SQL. HR sees compensation. Executives see everything.

None of these are enforced by telling Claude "don't tell sales reps about payroll." The tools don't exist for them. There's nothing to enforce.


The audit log

Every tool call gets logged. This is non-negotiable for any enterprise AI setup.

Your audit log should capture:

  • user_id — who made the call
  • tool_name — what they called
  • params — what they passed (sanitized if needed)
  • status — success or denied
  • timestamp — when
  • response_hash — a hash of what was returned (not the full response, but enough to reconstruct if needed)

The audit log answers two questions:

  1. Retroactive: "Who looked at customer X's billing data last Tuesday?" Now you can find out.
  2. Proactive: "Is anyone calling the payroll tool more than 20 times a day?" That's an anomaly worth investigating.
// Simple audit log table schema
// tool_calls (
//   id uuid primary key,
//   user_id text not null,
//   org_id text not null,
//   tool_name text not null,
//   params jsonb,
//   status text,  -- 'success' | 'denied' | 'error'
//   response_hash text,
//   duration_ms integer,
//   created_at timestamptz default now()
// )

What goes wrong without this

The trust event. An executive finds out that junior employees have been asking Claude to pull deal data, comp information, or budget numbers. Not because it was misused — because it could be. The enterprise AI program gets halted while a governance review happens. You lose two months.

The prompt-as-policy failure. You add "don't share financial data with non-finance users" to your system prompt. It works until it doesn't. Claude interprets a question differently than you expected. A clever user phrases the request in a way that bypasses the instruction. Prompt-as-policy is not enforcement — it's hope.

The scope creep problem. You add a new data source (say, your HR system) and it's technically available to all users because you didn't think about the access control when you wired it in. A week later someone pulls org data they shouldn't have. Now you're cleaning up instead of building.

The compliance problem. Financial data, PII, health information — many data types have regulatory implications for who can access them. "Claude just has access to everything" is not a defensible audit response.


What to build first

If you're building this from scratch, the order matters:

  1. Permission groups first. Define your groups before you define your tools. What roles exist in your organization? What data does each role legitimately need?

  2. Tool-level permission requirements. For each tool you build, define which permissions it requires. Start strict — you can always loosen.

  3. ACL middleware. Build the check-before-execute layer. This is the piece that makes everything else meaningful.

  4. Identity injection. Every tool call gets caller identity passed in automatically. No exceptions.

  5. Audit logging. From day one. Not retroactively.

  6. Tool list filtering. The session initialization step that builds Claude's available tool list based on the caller's permission group. Unauthorized tools are never offered.

This is a few days of engineering work. It's the foundation that everything else builds on. And unlike most infrastructure, the value shows up in what doesn't happen — the data exposure that didn't occur, the trust event that didn't derail your program.

Related tools

Next in Building Your Internal AI Stack · Step 7 of 8

Continue to the next article in the learning path

Next article →

Weekly brief

For people actually using Claude at work.

Each week: one thing Claude can do in your work that most people haven't figured out yet — plus the failure modes to avoid. No tutorials. No hype.

No spam. Unsubscribe anytime.

What to read next

Picked for where you are now

All articles →