Snowflake data movement policies: the copy path gets a guardrail
Masking decides what you can see. Data movement policies decide what you can take.
TL/DR: data movement policies hit general availability in Snowflake on Aug 19. One rule per movement type sets a row budget, policies bundle rules and hang off tags, with three possible outcomes: block, alert, or allow.
Governance in ❄️ Snowflake had an asymmetry I kept stumbling into: Masking policies and row access policies determine whether a role may view a column. Fine, that half of the story is settled. However, once a role can read sensitive data, nothing restrains it from shipping the entire table somewhere else. I met this issue (for example) in my post on exporting Snowflake data to Excel: reads were governed, copies were not.

Data movement policies (DMP) are built to seal that gap 🥳
The model: rules, policies, tags
The stack has three layers, modeled on the conditional governance Snowflake already ships. A (1) rule picks a single movement type and holds a MAX_ROWS expression. A (2) policy groups rules into a pair of lists. A (3) tag ties that construct to the data.
MAX_ROWS carries the weight... it returns an integer with three readings: NULL leaves a rule without effect, 0 refuses the operation, and any positive value caps how many rows that movement type may carry.
create or replace data movement rule pii_copy_hard_boundary
type = 'COPY_INTO_EXTERNAL_STAGE'
max_rows as () returns integer
-> (1000);
create or replace data movement rule pii_agent_guard
type = 'AGENT_ACCESS'
max_rows as () returns integer
-> (0);This way, one rule controls one movement type. The second rule above, which leaves agent access fully blocked, would have felt exotic a year back... today, with an MCP-style client coming out of every tool vendor, it appears not only reasonable, but rather urgent 😅
Enforce vs. alert
Rules aggregate to a policy under one of two headings: ENFORCE_RULES may refuse an operation outright, ALERT_RULES let it complete and raise a notification. The two tiers map onto a pattern I use almost everywhere: a generous alert threshold first, a firmer ceiling above that.
create or replace data movement rule pii_copy_alert_budget
type = 'COPY_INTO_EXTERNAL_STAGE'
max_rows as () returns integer
-> (500);
create or replace data movement policy pii_dmp
enforce_rules = (pii_copy_hard_boundary, pii_agent_guard)
alert_rules = (pii_copy_alert_budget)
comment = 'PII guardrails: alert >= 500, block >= 1000';
alter tag pii set data movement policy pii_dmp;The policy then binds to a tag at column, table, schema, or database scope, or falls back to the account as the baseline. Resolution starts at the most granular level that carries a policy and works down (or up? 🤔) toward the account. If one single query touches several policies at once, the lowest MAX_ROWS among them applies. Tag-based attachment is pretty neat: the tagging scheme already in place labels the sensitive domains, so the guardrail rides along with the tag rather than being welded onto every table individually.
What counts as movement
A rule's type must line up with the movement class Snowflake itself assigns to the query. Six classes exist t0day: COPY_INTO_EXTERNAL_STAGE, COPY_INTO_INTERNAL_STAGE, AGENT_ACCESS, SNOWSIGHT_UI, UI_DOWNLOAD, and PROGRAMMATIC_FETCH. Snowflake gives each statement exactly one primary type, picked in that fixed order. The consequence: a COPY INTO run by an agent still classifies as a COPY_INTO_* type, and an agent talking through a driver-like backend lands on AGENT_ACCESS before PROGRAMMATIC_FETCH.
A couple of classification quirks: Code running inside a stored procedure classifies as PROGRAMMATIC_FETCH even if the session that triggered it lives in Snowsight. However, UI_DOWNLOAD gets judged after the query finishes... crossing the threshold doesn't stop the query execution, it just disables the download control on the Snowsight results pane. Oh, and UI_DOWNLOAD accepts no alert rules whatsoever.
And then there is the agent angle: AGENT_ACCESS takes in agent and MCP-style clients. An access class that once read as an ordinary query over JDBC now produces its own visibility.
PARTITION BY are blocked outright whenever DMP is live on the tables involved, no matter what a rule says. And cross-region shares fall outside DMP's reach... this is an exfiltration control, not a replication mechanism.Rollout
Begin alert-only with roomy budgets, find out where movement genuinely occurs, then bring in enforce rules for the risky domains and tighten gradually. Alongside that, I would encode approved workflows directly in the rule body:
create or replace data movement rule pii_copy_hard_boundary
type = 'COPY_INTO_EXTERNAL_STAGE'
max_rows as () returns integer
-> (
case
when sys_context('SNOWFLAKE$SESSION', 'ROLE') = 'PII_TEAM_ROLE' then null
else 500
end
);Why express the exception as a CASE inside the rule and not as a separate grant? Because the carve-out is part of the movement workflow, not of the role's read privileges. The PII-team (what team is that again? 😅) keeps an uninterrupted export path, and no user holds a role that quietly dilutes DMP. SYS_CONTEXT additionally surfaces movement context, such as the destination stage's cloud provider and the name of the invoking agent, which lets a rule encode something like "exports to AWS may run to 1,000 rows, while every other destination smells wrong" 😉
Watching the watchers
Both the policy inventory and observed violations surface in ACCOUNT_USAGE.
select query_id, movement_type, user_name, data_entities, timestamp
from snowflake.account_usage.data_movement_violations
order by timestamp desc
limit 100;To set expectations about timing up front: the telemetry views can trail events by up to three hours, and violation capture is best effort. Enforcement occurs at query time, and the row that later appears in the view is an outcome, not the enforcement itself. One further trap for anyone migrating environments: cloned schemas carry over tag-to-policy bindings, yet cloned policies open with empty rule lists. Wire the rules in before trusting them, or the clone ends up with tags aimed at a policy that enforces nothing 😅
That would actually have been precisely the quiet failure DMP exists to eliminate... Oh well...

