SafeCoQuery
Pre-release · documentation for an unreleased build

SafeCoQuery Documentation

Last updated: 22 August 2026

SafeCoQuery sits between an AI coding agent and a MySQL database. It decides what the agent may see, holds writes for you to approve, and records what was disclosed. This page describes how that works in practice.

Requirements

Setting up a connection

A connection needs a host, port, user and password. Credentials go into the macOS Keychain; they are never written to a policy file or a config file.

Give SafeCoQuery a least-privilege account. It shapes what the agent sees, but it acts with whatever permissions the credential carries — see Limits.

Through an SSH bastion

If the database is not directly routable, a connection can tunnel through a bastion host. Two things are deliberately different from an ordinary SSH client:

Reviewing tables

The concept people get wrong

An unreviewed table is invisible to the agent — not merely unfiltered. Nothing is exposed because you have not got to it yet.

Review is per table, because that is the only granularity a human realistically finishes. Column-by- column sign-off across a real schema is the kind of task that gets abandoned halfway, leaving a policy nobody trusts.

That choice has a cost, and it is worth being blunt about it: inside a reviewed table, every column that no pattern matches is allowed. Reviewing a table is a statement that you are happy for the agent to read it, subject to the patterns. So the shipped default patterns have to be genuinely complete.

Why the defaults are what they are

The trust demo once caught card_number reaching an agent in clear, precisely because a reviewed table makes every unmatched column readable. That is why payment identifiers are in the shipped defaults, and why they are locked.

If you review a table with columns the defaults do not anticipate, add them explicitly before you enable agent access rather than after.

Policy

Policy is a file. It reviews in a pull request like anything else, and the defaults are deliberately unhelpful to an agent.

version: 1
connection: production-replica

# Anything nobody has classified yet. Both default
# to block: a column added next month is invisible
# until a human looks at it.
unknown_column_policy: block
unknown_table_policy: block
disclose_policy: full

# Patterns give coverage; explicit columns give
# correctness. Without patterns, drift fails open.
patterns:
  - match: [password, passwd, secret, token, apikey,
            privatekey, cvv, cvc, pin, hash, salt]
    policy: BLOCK
    locked: true   # an ALLOW cannot relax this

  - match: [card, card_number, pan, iban, sort_code,
            account_number]
    policy: MASK(card)
    locked: true

  - match: [email, phone, mobile, telephone, msisdn]
    policy: MASK(tokenize)

  - match: [ssn, nino, dob, birthdate, passport, licence]
    policy: MASK(redact)
    locked: true

# Explicit rules. These win unless a pattern is
# locked.
columns:
  customers.internal_notes: BLOCK
  customers.signup_source: ALLOW

# The agent's query is rewritten to include these;
# it cannot see or remove them.
rows:
  orders: "tenant_id = 42"

schemas:
  mysql: HIDDEN
  information_schema: HIDDEN

# Per-table sign-off. An unreviewed table is invisible.
reviewed:
  - customers
  - orders

Three things to understand

Connecting an agent

The order matters, and it is not guessable.

  1. Open the connection in SafeCoQuery. The agent can only reach a database you currently have open. No app running means no agent access.
    This is deliberate rather than a limitation: a human who is not there cannot approve a write or press stop.
  2. Enable agent access on that connection. It is off by default, and it is a positive, per-connection decision — not a global setting you forget you turned on.
  3. Copy the generated claude mcp add … command from the Agent connection panel and run it.
About the handle in that command

It is a selector and a revocation token, not a secret that protects anything. You hand it to the agent by design. The gate is the agent-access toggle, not the handle.

Regenerating the handle immediately tears down any live agent connection. It is a real revoke, not a rename.

Write approval

The agent proposes a write; it does not perform one. You are shown:

Approving binds that exact statement by hash, so an approved card cannot go on to execute different SQL. Execution runs inside a transaction that rolls back if the affected rows exceed the cap you set.

What the card does not show

It does not show before and after values for the affected rows. Reading those needs a privilege and audit story this build has not designed, so the app says so in its own UI rather than implying a preview it cannot give you.

The audit log

Every disclosure is recorded: the requested statement, the executed statement, the policy applied, and how many rows the agent received. Records are hash-chained, and the head of the chain is anchored in the macOS Keychain.

The agent cannot read it. Every audit method is refused on the agent's connection, and refused in a way that is indistinguishable from a method that does not exist.

Export writes a file wherever you choose. It carries the original bytes of each record rather than a re-serialisation, so a third party can re-walk the chain and confirm nothing was altered, reordered or removed. If verification fails, the export still happens and is marked as failed — a tampered log is exactly what you would want to hand an investigator.

What the log identifies

It records the principal and the session. It does not identify which agent connected: those fields are declared but not populated in this build. “Which session, and how many rows” is what you can rely on.

STOP AI

STOP AI cancels work that is already streaming. It reaches the database server to kill the running query rather than just dropping the results on the floor, and the revocation survives quitting the app — you do not have to stay running to stay stopped.

Limits

These are the same limits stated in the Terms of Use. They belong here too, because knowing where the boundary is tells you how to deploy this sensibly.

The one to internalise

Masking is a transmission-time transform. It is applied to results passing through SafeCoQuery. It does not alter what is stored, and anything holding a database credential and a network route can read the raw table with SafeCoQuery not involved at all.

Troubleshooting

client sent no build id; refusing to trust an unstamped client

The app and the daemon must agree on what they were built from, and an unstamped client cannot prove it. Rebuild the app. If you built the daemon yourself, run ./scripts/build_daemon.sh first.

A SafeCoQuery service is already running at this socket that this app instance did not start

A previous daemon outlived its app. Quit it and reopen SafeCoQuery so it starts its own. This can happen after a crash or a force-quit — it is a known rough edge rather than a sign something is wrong with your setup.

…was built from different daemon/ source than this Xcode build sees right now

Developer-only. Re-run ./scripts/build_daemon.sh.