mcpbeat Sign in

Safe SQL Execution Agent Skill

>- Use whenever code will build, return, fetch, or execute SQL that runs against a user's real Postgres database — even when the request reads like an ordinary feature or bug fix and never says "security," "injection," or builder, or endpoint that builds/returns SQL for database objects (tables, views, functions, DB triggers, indexes, RLS policies); interpolating a schema/table/column/search/route-param value into SQL text; storing, fetching, or re-running SQL that round-trips from the database (a policy's definition, a function/view definition, a snippet's saved content); and any "Run"/"Apply"/"Execute" action that sends SQL to a project's database (SQL editor run-selection, policy editor apply, snippet runner). Load this BEFORE writing such code, not only when reviewing a finished diff. Skip only for changes that never touch SQL text or execution — styling, unrelated data hooks, non-SQL form validation, or UI layout work.

4k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
107520
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/supabase/supabase --skill safe-sql-execution

The instruction itself

15 sections, as written by the author

Safe SQL execution

Supabase Studio executes SQL statements directly against the user's database.

Because this is the authenticated user's own database, our security model is

different from most frontend applications: a user should be able to execute any

SQL statement, as long as it is proven that they themselves authored it. What

we SHOULD NOT ALLOW is execution of SQL statements that can be influenced by an

attacker, such as through URL parameters.

Security model

The security model for SQL execution in Supabase Studio is based on the

principle of "proven authorship". This means that a user should only be able to

execute SQL statements that they have explicitly authored, and not statements

that can be influenced by external input.

There are three classes of SQL fragments:

  • Hardcoded within the application code. These are safe to execute because

they cannot be influenced by an attacker. They can be marked with the

safeSql utility with pg-meta:

    import { safeSql } from '@supabase/pg-meta'

    const sql = safeSql`
      SELECT *
      FROM users
      WHERE id = 1
    `

safeSql automatically creates a string of the branded type

SafeSqlFragment. (See Provenance Tracking below.)

  • Third-party influenceable. These are SQL fragments that can be influenced

by an attacker, such as through URL parameters or LLM output. These should

be marked with the untrustedSql utility with pg-meta:

    import { untrustedSql } from '@supabase/pg-meta'

    const unsafeQuery = searchParams.get('query')
    const querySql = untrustedSql(unsafeQuery)

untrustedSql creates a string of the branded type UntrustedSqlFragment.

(See Provenance Tracking below.)

  • User-authored. These are SQL fragments that are authored by the user

themselves within the UI, for example in a text input field. Because the

user is the author, these should be considered safe to execute.

However, there is a caveat, where third-party and user-authored code can

mix, contaminating the user-authored code (for example, if an input is

prefilled from an unsanitized URL parameter). Provenance tracking helps us

track these cases.

For example, a safe input component could be implemented as follows by requiring that its placeholder and controlled value are of type SafeSqlFragment. In this case we can use its onChange to promote the user input to SafeSqlFragment type, because we know that the user is the author of the input. An implementation of this is in

@apps/studio/components/ui/SafeSqlInput.tsx:

    import { rawSql, type SafeSqlFragment } from '@supabase/pg-meta'
    import type { ChangeEvent, ComponentProps } from 'react'
    import { Input } from 'ui-patterns/DataInputs/Input'

    type InputProps = ComponentProps<typeof Input>

    export type SafeSqlInputProps = Omit<
      InputProps, 'placeholder' | 'value' | 'onChange'
    > & {
      placeholder?: SafeSqlFragment
      value: SafeSqlFragment
      onChange?:
        (event: ChangeEvent<HTMLInputElement>, value: SafeSqlFragment) => void
    }

    export const SafeSqlInput = ({ onChange, ...props }: SafeSqlInputProps) => (
      <Input
        {...props}
        onChange={(event) => onChange?.(event, rawSql(event.target.value))}
      />
    )

This is pretty much the ONLY VALID USE CASE of the rawSql export from

pg-meta, and it should be used with caution.

Provenance tracking

Branded types are used to track the provenance of SQL fragments. The types,

exported from pg-meta, are:

  • SafeSqlFragment: represents SQL fragments that are safe to execute, because

they are either hardcoded in the application or authored by the user

themselves.

  • UntrustedSqlFragment: represents SQL fragments that can be influenced by an

attacker, such as through URL parameters or LLM output.

These are valid ways to generate a SafeSqlFragment:

  • Using the safeSql utility from pg-meta to create hardcoded SQL fragments.
  • Using the sanitization utilities from pg-meta to sanitize untrusted input

and promote it to a SafeSqlFragment:

  • ident
  • literal
  • keyword
  • Using the safe SQL manipulation utilities:
  • joinSqlFragments
  • trimSafeSqlFragment

UntrustedSqlFragments can be generated from raw strings using

untrustedSql().

There is also a union type, DisplayableSqlFragment, which represents SQL fragments that can be safely displayed in the UI, but not necessarily executed. This includes both SafeSqlFragment and UntrustedSqlFragment.

Security of SQL round-tripped from the user's database

SQL derived directly from catalog tables (e.g., function definitions, RLS

expressions, etc.) is considered safe, and it is promoted AT THE POINT OF

BEING QUERIED from the database. In most cases, this is in an

apps/studio/data/\*_/_.ts file, in the utility function that makes the API or

database fetch.

A critical exception to the safety of SQL round-tripped from the database is

user snippets. These must NEVER BE CONSIDERED SAFE because they are both (a)

externally influenceable and (b) auto-saved. The snippet type uses the

unchecked_sql property, which is an UntrustedSqlFragment, to enforce this.

Promoting SQL fragments to SafeSqlFragment type

Given an insecure string or UntrustedSqlFragment, how do we promote it safely

to a SafeSqlFragment?

Sanitization utilities

This is the preferred method when the input is sanitizable, e.g., it is a

relation name, a column name, will be compared as a literal, etc.

The pg-meta library provides the following sanitization utilities that can be

used to safely promote untrusted input to SafeSqlFragment:

  • ident: for sanitizing identifiers such as table names or column names.
  • literal: for sanitizing literal values that will be used in SQL statements.
  • keyword: for sanitizing SQL keywords.

acceptUntrustedSql

Some untrusted SQL fragments cannot be sanitized with the above utilities. For

example, the USING expression in the RLS policy editor is an arbitrary SQL

expression.

In these cases, we can promote the SQL fragment _upon explicit user action_.

User action indicates that the user has seen the SQL and is OK with running it.

For example, an explicit user action could be clicking a "Run" button.

The promotion happens with the acceptUntrustedSql utility from pg-meta,

which takes an UntrustedSqlFragment and returns a SafeSqlFragment.

This utility MUST ONLY BE USED IN event handlers. It should NEVER be used in

a useQuery, direct in the render body of a component, in a useEffect, or

anywhere it could auto-run without explicit user action.

This is safe:

import { acceptUntrustedSql } from '@supabase/pg-meta'

function SafeComponent() {
  const { mutate: execute } = useExecuteSqlMutation()

  const handleRun = () => {
    // ✅ GOOD: Safe because it is in an event handler which requires a user
    // click
    execute({ sql: acceptUntrustedSql(/* sql */) })
  }

  return (
    <button onClick={handleRun}>Run</button>
  )
}

This is unsafe:

import { acceptUntrustedSql } from '@supabase/pg-meta'

function UnsafeComponent() {
  const { data } = useQuery({
    queryKey: ['execute-sql', sql],
    queryFn: () => {
      // 🛑 BAD: Unsafe because it is in a query which could auto-run without
      // explicit user action
      return execute({ sql: acceptUntrustedSql(/* sql */) })
    },
  })
}

Type guarantees

SQL run against the user's Postgres database runs through the executeSql

function, which only takes arguments of type SafeSqlFragment for the SQL

parameter. Raw strings or UntrustedSqlFragments will error at compile time.

Examples

Hard-coded SQL

// ✅ GOOD: Automatically safe with `safeSql` utility
const selectStatement = safeSql`select 1`

SQL with sanitizable interpolations

// ✅ GOOD: `pg-meta` utilities sanitize the input
const tableName = ident(userInputTableName)
const searchString = literal(userInputSearchString)
const sqlStatement = safeSql`
  SELECT *
  FROM ${tableName}
  WHERE search_column = ${searchString}
`
// 🛑 BAD: Passing raw strings will type error
const tableName = 'my_table'
const sqlStatement = safeSql`
  SELECT *
  FROM ${tableName}
`

Non-sanitizable SQL from a user input

// ✅ GOOD: SafeSqlInput only allows a value that is a SafeSqlFragment
import { SafeSqlInput } from '@apps/studio/components/ui/SafeSqlInput'

function MyComponent() {
  const [sql, setSql] = useState<SafeSqlFragment>(safeSql``)

  return (
    <SafeSqlInput
      placeholder={safeSql`Enter your SQL query here...`}
      value={sql}
      onChange={(event, value) => setSql(value)}
    />
  )
}
// 🛑 BAD: This input mixes SafeSqlFragments and unsafe strings

function MyBadComponent() {
  const [sql, setSql] = useState<SafeSqlFragment>(safeSql``)

  return (
    <Input
    // 🛑 BAD: This is unsafe because the placeholder is a raw string
      placeholder="Enter your SQL query here..."
      value={sql}
      onChange={(event) => setSql(event.target.value)}
    />
  )
}

Round-tripping SQL from the database (NOT snippet content)

// ✅ GOOD: SQL from the database is promoted to SafeSqlFragment at the point
// of fetching

// data/function-definitions.ts
function markFunctionDefinitionSafe(
  functionDefinition: FunctionDefinition
): SafeFunctionDefinition {
  return {
    ...functionDefinition,
    definition: functionDefinition.definition as SafeSqlFragment,
  }
}

// data/function-definitions.ts
function getFunctionDefinitions() {
  return GET(`/function-definitions`).then((functionDefinitions) =>
    functionDefinitions.map(markFunctionDefinitionSafe)
  )
}
// 🛑 BAD: Strings are promoted to SafeSqlFragment in a utility function, where
// it is impossible to easily determine the safety of the input

// utils.ts
function markFunctionDefinitionSafe(
  functionDefinition: FunctionDefinition
): SafeFunctionDefinition {
  return {
    ...functionDefinition,
    definition: functionDefinition.definition as SafeSqlFragment,
  }
}

// Component.ts
function MyComponent() {
  const { data: functionDefinitions } = useFunctionDefinitions()
  const safeFunctionDefinitions = functionDefinitions.map(markFunctionDefinitionSafe)
}

Snippet content is ALWAYS UNSAFE

Snippets are auto-persisted to the database and can be created or modified

through externally influenceable channels (e.g., prefilled from URL params).

The unchecked_sql property is typed as UntrustedSqlFragment to enforce this

— it must only be promoted to SafeSqlFragment via acceptUntrustedSql in an

event handler that requires explicit user action.

// 🛑 BAD: Snippet content is executed automatically via useQuery, with no
// explicit user action confirming that the user has reviewed the SQL.
import { acceptUntrustedSql } from '@supabase/pg-meta'

function UnsafeSnippetPreview({ snippet }: { snippet: Snippet }) {
  const { data } = useExecuteSqlQuery({
    sql: acceptUntrustedSql(snippet.content.unchecked_sql),
  })

  return <Results data={data} />
}
// 🛑 BAD: Casting bypasses the type system entirely. The snippet's
// `unchecked_sql` is `UntrustedSqlFragment` for a reason — never cast it.
function UnsafeSnippetRunner({ snippet }: { snippet: Snippet }) {
  const { mutate: execute } = useExecuteSqlMutation()

  useEffect(() => {
    execute({ sql: snippet.content.unchecked_sql as SafeSqlFragment })
  }, [snippet])
}
// ✅ GOOD: Snippet content is only promoted to SafeSqlFragment inside an event
// handler, after the user clicks Run. The user has seen the SQL in the editor
// and explicitly chosen to execute it.
import { acceptUntrustedSql } from '@supabase/pg-meta'

function SnippetRunner({ snippet }: { snippet: Snippet }) {
  const { mutate: execute } = useExecuteSqlMutation()

  const handleRun = () => {
    execute({ sql: acceptUntrustedSql(snippet.content.unchecked_sql) })
  }

  return (
    <>
      <SnippetEditor snippet={snippet} />
      <button onClick={handleRun}>Run</button>
    </>
  )
}

Analytics SQL (BigQuery / ClickHouse)

The same security model applies to analytics queries, which target BigQuery

or ClickHouse via the

/platform/projects/{ref}/analytics/endpoints/logs.all{,.otel} endpoints.

Filter keys and values from URL parameters and UI inputs are spliced into SQL

that runs against the project's logs, so the same injection risk exists.

The brand and helpers live in apps/studio/data/logs/safe-analytics-sql.ts,

intentionally disjoint from the pg-meta SafeSqlFragment brand:

  • SafeLogSqlFragment — branded type for analytics SQL.
  • safeSql — template tag that only accepts SafeLogSqlFragment

interpolations.

  • analyticsLiteral(value) — sanitizes string/number/boolean literals.
  • quotedIdent(name) — validates and backtick-quotes dotted identifiers.
  • keyword(value, allowed) — validates against an allow-list of operators.
  • joinSqlFragments(fragments, separator) — composes already-branded

fragments.

The brands are kept separate because escape semantics differ — Postgres-safe

E'…' strings, ::jsonb casts, and double-quoted identifiers are unsafe for

BigQuery and/or ClickHouse, and vice versa. Crossing the brands would silently

emit unsafe SQL.

The wire-boundary wrapper is executeAnalyticsSql in

apps/studio/data/logs/execute-analytics-sql.ts, analogous to pg-meta's

executeSql. It accepts only SafeLogSqlFragment for its sql parameter, so

raw strings are rejected at compile time. A grep-based vitest

(apps/studio/tests/unit/lints/analytics-sql-boundary.test.ts) prevents

regressions by failing the build if any file outside

execute-analytics-sql.ts calls post() or get() directly against

logs.all or logs.all.otel.

import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
import { analyticsLiteral, quotedIdent, safeSql } from '@/data/logs/safe-analytics-sql'

// ✅ GOOD: every interpolation is sanitized.
const sql = safeSql`
  SELECT timestamp, event_message
  FROM ${quotedIdent(table)}
  WHERE id = ${analyticsLiteral(id)}
`

await executeAnalyticsSql({
  projectRef,
  endpoint: '/platform/projects/{ref}/analytics/endpoints/logs.all',
  sql,
  iso_timestamp_start,
  iso_timestamp_end,
})
// 🛑 BAD: raw string interpolation. This fails to type-check at the
// executeAnalyticsSql boundary because the result is `string`, not
// `SafeLogSqlFragment`.
const sql = `SELECT * FROM ${table} WHERE id = '${id}'`
await executeAnalyticsSql({ projectRef, endpoint, sql, ... })

Other skills for the same job

different authors, same section of the catalogue
Node Link And Diagram Layout
by openai
vendor

Choose and apply automatic layout strategies for node-link diagrams and connected-node visuals. Use when the user asks how to auto-arrange nodes, reduce line crossings, route edges, avoid overlaps, stabilize layout, or choose graph-layout algorithms for network diagrams, dependency graphs, database schema diagrams, ERDs, state machines, decision trees, flow diagrams, box-and-line editors, or other line-connected nodes.

6k tokens
Web Design
by xiaopu-ai

Web 视觉设计 SKILL。输入 PRD / 参考 URL / 截图 / 关键词(任意组合),先产出一份标准化 DESIGN.md 设计规范,用户确认后据此生成 UI/UX、视觉、动效、响应式全部达标的 web 代码。专攻 web 端:Landing Page、Portfolio、产品页、博客、个人站、SaaS 介绍页等。当用户说"帮我做个网站""设计一个页面""参考 XX 做一个""把这个截图/PRD 做成网页""做一个 landing page""出一份 design 规范"时触发。不用于后端、数据库、纯逻辑 bug 修复。

621k tokens scripts zh
Django Patterns
by loulanyue

Django架构模式,使用DRF设计REST API,ORM最佳实践,缓存,信号,中间件,以及生产级Django应用程序。

5k tokens
DrugComb
by BioTender-max

> Query the DrugComb drug combination database for cancer cell-line synergy and sensitivity data. Use whenever the user asks about drug combinations, synergy scores (ZIP/Bliss/Loewe/HSA), combination sensitivity (CSS), or wants to look up how two drugs interact in a specific cancer cell line.

4k tokens scripts
Senior Architect
by Infrasity-Labs

This skill should be used when the user asks to "design system architecture", "evaluate microservices vs monolith", "create architecture diagrams", "analyze dependencies", "choose a database", "plan for scalability", "make technical decisions", or "review system design". Use for architecture decision records (ADRs), tech stack evaluation, system design reviews, dependency analysis, and generating architecture diagrams in Mermaid, PlantUML, or ASCII format.

31k tokens scripts
Django Patterns
by mturac

Django架构模式,使用DRF设计REST API,ORM最佳实践,缓存,信号,中间件,以及生产级Django应用程序。

5k tokens
Senior Architect
by aAAaqwq

This skill should be used when the user asks to "design system architecture", "evaluate microservices vs monolith", "create architecture diagrams", "analyze dependencies", "choose a database", "plan for scalability", "make technical decisions", or "review system design". Use for architecture decision records (ADRs), tech stack evaluation, system design reviews, dependency analysis, and generating architecture diagrams in Mermaid, PlantUML, or ASCII format.

31k tokens scripts
Thinking Antirez
by aAAaqwq

蒸馏antirez(Salvatore Sanfilippo)思维模式的实用框架——极简代码哲学、Redis设计哲学、工程美学

4k tokens zh

How to use it

Copy the folder

Take supabase/safe-sql-execution from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.