Your agent logs in as you. That is the problem.

On October 30, Snowflake made Restricted Session Scopes generally available. The idea is simple: when an agent is active in a session, it gets a privilege ceiling that sits below the user's own rights. Their words: it "can only restrict what agents can do on behalf of the user, never expand user privileges."

The same day, Wiz published "Black October" for AI Developer Platforms, listing at least nine AI developer platforms that disclosed incidents this month, with attackers "explicitly hunting stored environment variables" that grant AI access. Different story, same lesson. Whatever access your agent holds is access someone else can end up steering.

You do not need Snowflake to apply the pattern. Here is the tiny version.

The rule: agent access = user access ∩ task ceiling

A human with ACCOUNTADMIN exploring data is fine. An agent with ACCOUNTADMIN reading a poisoned doc is not. So the agent never gets the user's full grants. It gets the intersection of:

  1. What the user can do (the agent can never exceed this).
  2. A ceiling for agent sessions (the agent can never exceed this either).

Snowflake's example ceiling is "read everywhere, write only to a sandbox database, block the admin and PII governance roles." Let's build exactly that.

The code

// A tiny session ceiling: the agent gets the intersection of what the
// user can do and what the task scope allows. It can only shrink access.
type Privilege = "read" | "write" | "admin";
type Grant = { privilege: Privilege; database: string };

const user: { grants: Grant[]; roles: string[] } = {
  grants: [
    { privilege: "read", database: "*" },
    { privilege: "write", database: "*" },
    { privilege: "admin", database: "*" },
  ],
  roles: ["ANALYST", "ACCOUNTADMIN", "DATA_GOVERNOR"],
};

// "Read everywhere, write only to the sandbox, never the admin roles."
const ceiling = {
  allow: [
    { privilege: "read", database: "*" },
    { privilege: "write", database: "AGENT_SANDBOX" },
  ] as Grant[],
  blockedRoles: ["ACCOUNTADMIN", "SYSADMIN", "SECURITYADMIN", "DATA_GOVERNOR"],
};

const covers = (g: Grant[], p: Privilege, db: string) =>
  g.some((x) => x.privilege === p && (x.database === "*" || x.database === db));

type Action = { tool: string; privilege: Privilege; database: string; role: string };

function check(a: Action, isAgent: boolean): string {
  if (!covers(user.grants, a.privilege, a.database) || !user.roles.includes(a.role))
    return "DENY (user lacks it)";
  if (!isAgent) return "ALLOW";
  if (ceiling.blockedRoles.includes(a.role)) return `DENY (role ${a.role} blocked for agents)`;
  if (!covers(ceiling.allow, a.privilege, a.database))
    return `DENY (${a.privilege} on ${a.database} is above the agent ceiling)`;
  return "ALLOW";
}

const actions: Action[] = [
  { tool: "query_orders", privilege: "read", database: "PROD", role: "ANALYST" },
  { tool: "create_temp_table", privilege: "write", database: "AGENT_SANDBOX", role: "ANALYST" },
  { tool: "drop_table", privilege: "write", database: "PROD", role: "ANALYST" },
  { tool: "unmask_pii", privilege: "read", database: "PROD", role: "DATA_GOVERNOR" },
  { tool: "grant_role", privilege: "admin", database: "PROD", role: "ACCOUNTADMIN" },
];

for (const a of actions) {
  console.log(`${a.tool.padEnd(18)} human: ${check(a, false).padEnd(6)} agent: ${check(a, true)}`);
}
Enter fullscreen mode Exit fullscreen mode

Run it with npx tsx ceiling.ts:

query_orders       human: ALLOW  agent: ALLOW
create_temp_table  human: ALLOW  agent: ALLOW
drop_table         human: ALLOW  agent: DENY (write on PROD is above the agent ceiling)
unmask_pii         human: ALLOW  agent: DENY (role DATA_GOVERNOR blocked for agents)
grant_role         human: ALLOW  agent: DENY (role ACCOUNTADMIN blocked for agents)
Enter fullscreen mode Exit fullscreen mode

Same user, same tools. The human can drop a prod table. The agent cannot, because the ceiling says writes only land in AGENT_SANDBOX. The agent also cannot switch into DATA_GOVERNOR to unmask PII, even though the user holds that role.

Three things this gets right

It can only shrink. The first check is the user's own grants. The ceiling is applied after, so a bad ceiling config can lock the agent out but can never hand it something the user does not have.

It keys on "is this an agent session," not on the prompt. Prompt injection defenses that read the text are guesses. This check does not care what the model was told. If a malicious Markdown file convinces the agent to run drop_table, the answer is still DENY.

Start read-only, then widen. Snowflake ships a built-in read-only scope and says many customers start there and tailor later. Same advice for your harness: begin with read only, log every DENY, and add a sandbox write grant when the logs show you need one.

Where to put it

Put the check in the tool layer, not in the system prompt. Every tool call goes through check(action, isAgent) before it touches anything. If you already run an approval gate, the ceiling sits in front of it: things above the ceiling are denied outright, and things under it can still require a human click.

If you want the per-task version of this, where each task gets a short-lived grant instead of a fixed ceiling, see tiny-access-grant in the Tiny Agent Marketplace.

Want the full harness?

This is one layer of what I teach in Ship a Safe Agent: permissions, approval gates, budgets and prompt injection defenses for agents you actually run in production. Module 1 is free, so start there: bhjr.dev/courses/ship-a-safe-agent/module-1. The full course is $149 until Nov 8 (then $199).

Code for this post: ceiling.ts and expected output. Example data only, no API key, no model calls.