# Safety

OKModel refuses a call that would read an unbounded set, write an unbounded set, or set a field the caller does not own. A destructive migration step is an error until the file acknowledges it. Protection of a target is off until you turn it on.

## The problem

A default that runs the wide query, or drops the table, fails open — it proceeds when it should have stopped. The mistake is one missing argument. The repair is a production incident.

## How OKModel handles it

These are the refusals that hold when you have not opted in. The linked page is the detail.

| Refusal | Page |
| --- | --- |
| `find`, `page`, a grouped `aggregate`, or a to-many `include` with no limit | [Query limits](/docs/operate/query-limits) |
| `update` or `delete` with no effective filter, and the same rule for `archive` and `restore` | [Write rows](/docs/queries/write-rows) and [Traits](/docs/schema/traits) |
| An unknown field on insert or update | [OKM1120](/docs/reference/errors#okm1120) |
| A guarded field set from input | [Visibility](/docs/schema/visibility) |
| A destructive migration step, until the file carries `-- okm-allow` | [Lint](/docs/migrations/checks-and-lint) |

`find` without a limit is OKM1101. A to-many `include` without a limit is OKM1105. `.all(reason)` is the hatch for a read or a write you mean to leave unbounded.

Leave `limits` out and those calls still refuse a missing limit:

```ts title="limits-before.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", { id: t.identity(), title: t.text() });
const app = schema({ tables: [notes] });

export async function read(url: string) {
  await using db = connect(url, { schema: app });
  return await db.notes.find({ where: { id: "1" } });
  // OkmError OKM1101: find() needs a limit.
}
```

```text title="limits-before.out"
find() needs a limit.
```

The fix on the error is `Pass a limit.`

Pass `limits` on `connect` and the same call uses that default:

```ts title="limits-after.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", { id: t.identity(), title: t.text() });
const app = schema({ tables: [notes] });

export async function read(url: string) {
  await using db = connect(url, {
    schema: app,
    limits: { find: 50 },
  });
  return await db.notes.find({ where: { id: "1" } }); // limit 50
}
```

They do not gain a silent default when `limits` is omitted. Detail: [Query limits](/docs/operate/query-limits).

`update` and `delete` without a filter are OKM1102. `.all(reason)` is the hatch, and a blank reason is refused. `archive` and `restore` use the same check.

An unknown key is refused and names the key. It is not stored. A guarded column cannot be set from input (OKM1190). Hidden and sensitive change what a row contains and what a log shows. The three flags are [Visibility](/docs/schema/visibility).

Dropping a table or a column is an error finding (OKM1511, OKM1512). An error finding fails the command as OKM1510. `-- okm-allow CODE: reason` on that step silences the code it names. A blank reason is itself OKM1510. The other codes are [Errors](/docs/reference/errors).

A protected target is a flag on the target, or a fingerprint in the user store. It is not implied by the name `production` or by `NODE_ENV`. `connect` does not read it. Which commands it blocks is [Protected targets](/docs/operate/protected-targets-and-roles).

## Prompt injection through error text and database values

Text from a database error, a catalog, or a user query is an object with `untrusted: true`, an origin, and a bounded `text`. The human line is `untrusted <origin>: "<text>"`. It is not an instruction. The cap is 200 code points for one string and 2,000 for one field. A caller may lower a cap and may not raise one. Bidi overrides and tag characters are removed from that text. Detail: [Untrusted text](#untrusted-text).

## Destructive migrations

Dropping a table, renaming a column, or adding a unique constraint on a table that already exists is an error finding, [OKM1510](/docs/reference/errors#okm1510). `okm generate` still writes the file and exits 1. `okm migrate plan` and `okm check` exit non-zero. `okm migrate apply` refuses with OKM1510 before any statement. A plan whose operation class is `destructive` sets `requiresAck`. Apply does not ask, and it does not read that flag. The only way past a destructive step is a `-- okm-allow CODE: reason` line in the committed migration file. No command-line flag does it. `okm check` and `okm migrate check` list those lines, and a boilerplate reason is warning [OKM2300](/docs/reference/errors#okm2300). A person reviews the line in the pull request, and a protected target still needs a terminal.

The lint codes are [Lint](/docs/migrations/checks-and-lint#lint-codes).

## Wrong-target runs

A target is protected when project config sets `protected: true` or when `okm protect add` has stored its fingerprint. The fingerprint is `hostDb`, the SHA-256 of lower-cased `host:port/database`. The user, the password, and the query string are not part of it. A state-changing command against a protected target exits 4. `okm migrate check` refuses a protected target, and `--allow-protected` does not lift that refusal. Omitting `--target` when several targets are configured is [OKM1853](/docs/reference/errors#okm1853) for commands that must pick one database.

Detail: [Protected targets](/docs/operate/protected-targets-and-roles) and [Production checklist](/docs/operate/production-checklist).

## Credential leakage

The protection store holds hashes, not URLs. Command output does not print the URL. `okm protect list` prints fingerprints. JSON for that command has `identity` null and does not include the URL.

[`okm doctor --bundle`](/docs/reference/cli/check#doctor---bundle) writes a diagnostics file for a person to send. It holds versions, codes, closed artifact names with hashes, and counts. It holds no connection string, host, user, URL, table or column name, SQL text, row value, absolute path, or environment variable value, and it opens no socket.

[`okm doctor --production`](/docs/reference/cli/check#doctor---production) reads one target in a `READ ONLY` transaction and makes no network call beyond that database. It names the target by its configured name. It never prints the URL, host, user, or password, and a failed connection is reported without the driver's text, which can name the host or the user.

## Untrusted text

Text read from a database catalog, a database error, or a user query is bounded and marked untrusted; this reduces risk and does not eliminate it.

Each value is an object. The keys are `untrusted`, `origin`, `text`, `truncated`, and `modified`. One string keeps at most 200 code points. One response field keeps at most 2,000. A caller may set a lower cap. A caller may not raise one.

The human line is `untrusted <origin>: "<text>"`. That line stands on its own. It is not an instruction.

`okm migrate plan --json` adds `operationClass` on the plan and on each step, `fromCatalogHash`, `toCatalogHash`, `planHash`, `requiresAck`, and `destructiveOperations`. `planHash` is the SHA-256 of canonical JSON — a fixed key order and encoding so the same plan always hashes the same — for format 1, the dialect, the two catalog hashes, and each step's SQL as executed with its operation class. The hash omits the target, timestamps, and the OKModel version. `requiresAck` is true when the plan's operation class is `destructive`. The plan command does not ask, and apply is unchanged.

`okm init --json` stores `data.detection.packageName` as an untrusted object with origin `package.json`. The plan hash omits that name. A finding `reason` is an untrusted object with origin `user.query`. `fix` stays a string written by OKModel. A database error's text is `errors[].detail`, and the summary stays the registry sentence. `okm migrate status` JSON stores `steps[].id`, `kind`, and `state` as untrusted objects. The text table is unchanged. `okm migrate baseline --json` stores each unmanaged `name` as an untrusted object.

## What this does not defend against

- A program running as the same operating-system user can edit the protection file, delete it, or set `protected: false`. The grant in [roles](/docs/operate/protected-targets-and-roles) is the boundary that still holds.
- `default_transaction_read_only` is a session setting. A session can turn it off. Roles use `GRANT`, not that setting. See [config roles](/docs/reference/config-roles).
- Marking text untrusted reduces the chance an agent obeys it. It does not eliminate that chance.
- [OKM1538](/docs/reference/errors#okm1538), a type change that rewrites the heap, is a warning. Apply does not refuse it. The recipe is [a type change](/docs/reference/type-change). There is no separate command for it.
- A login that is a superuser, or that holds `CREATEROLE` and ownership, can do more than the reader role in [roles](/docs/operate/protected-targets-and-roles).

## Limits and trade-offs

Validation does not run on every write. Omitted, or `false`, leaves it off. A column, a table, or the schema can turn it on. The switch is [Validation](/docs/schema/validation).

The refusals above are the library. They are not a review of the package you install. Pinning the lockfile, waiting before a new release, and reading a provenance attestation are [Safe use](/docs/operate/using-okmodel-safely).

What to turn on before a real target — `protected`, `requireMeta`, `--target`, a direct connection — is the [Production checklist](/docs/operate/production-checklist). This page does not repeat that list.

## Where to read the detail

| Page | You use it for |
| --- | --- |
| [Query limits](/docs/operate/query-limits) | `limits`, `max`, and `.all(reason)`. |
| [Protected targets](/docs/operate/protected-targets-and-roles) | The flag, the store, and the grants. |
| [Safe use](/docs/operate/using-okmodel-safely) | The lockfile, the wait, and provenance. |
| [Production checklist](/docs/operate/production-checklist) | What a production target should have on. |
| [Lint](/docs/migrations/checks-and-lint) | Destructive findings and `-- okm-allow`. |

Index: [/llms.txt](/llms.txt)
