Show HN: Owthorize: catch destructive AI-agent tool calls before they run

ayushpawar1 pts0 comments

owthorize - npm

npm

Search<br>Sign UpSign In

owthorize

0.4.2 • Public • Published 2 months ago<br>Readme<br>Code Beta<br>3 Dependencies<br>0 Dependents<br>2 Versions<br>owthorize

Prompt safeguards are theater. The model isn't the boundary — the layer between it and your systems is.

owthorize is a synchronous JS/TS gate that catches destructive AI-agent tool calls before they execute , using ASTs and parsed shapes — not regex on strings. It sits at the tool layer of your agent (OpenAI, Anthropic, LangChain, Vercel AI SDK) and gates every call through a small rule engine.

// the model emits this:<br>db.query("DROP TABLE users")<br>// │<br>// ▼ guard.tool() intercepts, parses, denies<br>// │<br>throw new GuardDenied("DDL not allowed: drop")

The problem

Modern AI agents call tools. Tools have side effects. Three things go wrong:

Prompt injection. A user (or a document the agent reads) coerces the model into emitting calls the developer never anticipated.

Hallucinated arguments. The model forms a syntactically-valid call with semantically-wrong inputs — DELETE FROM users instead of DELETE FROM users WHERE id = ?.

Reasoning errors. The model tries to "be helpful" by issuing destructive cleanup, fetches an internal IP it shouldn't reach, or writes outside the workspace.

Prompt-level safeguards ("you are a helpful assistant, do not drop tables") fail under all three. The right boundary is the layer between the model and your database/HTTP/disk — the tooling layer. That's where owthorize lives.

What it catches

SQL DDL — DROP, TRUNCATE, ALTER, CREATE, RENAME (Postgres / MySQL / SQLite, AST-level)

Unbounded mutations — UPDATE / DELETE with no WHERE clause

SSRF targets — RFC1918, link-local, loopback, AWS metadata 169.254.169.254, IPv4-mapped IPv6 ([::ffff:127.0.0.1]), *.internal wildcards

Shell metacharacters and dangerous commands — basename match (/usr/bin/rm → rm), pipe / redirect / $() / backtick detection

Path traversal — resolved-path containment, prefix-collision-safe (root /safe doesn't match /safe-evil)

Project-specific policy — typed custom rules per adapter (rules.sql.custom, rules.http.custom, etc.)

Who it's for

JS/TS developers shipping AI agents that touch:

a database (Postgres, MySQL, SQLite — via mysql2, pg, sqlite3, etc.)

outbound HTTP (fetch, axios, undici)

the filesystem (fs/promises)

shell exec (child_process)

If your agent is read-only against trusted data, you probably don't need this. If it can write — or if it can fetch arbitrary URLs — owthorize closes the most-likely failure modes before they ship.

Install

npm install owthorize

Both ESM and CJS are supported. Node ≥ 18.

Quickstart

import { Guard, rules, GuardDenied } from "owthorize"

const guard = new Guard({<br>rules: [<br>rules.sql.denyDDL(),<br>rules.sql.denyMutationWithoutWhere(),<br>rules.http.denyHosts(rules.http.SSRF_DEFAULTS),<br>],<br>})

const safeQuery = guard.tool("db.query", {<br>adapter: "sql.postgres",<br>handler: async ({ query }: { query: string }) => db.query(query),<br>})

try {<br>await safeQuery({ query: "DROP TABLE users" })<br>} catch (err) {<br>if (err instanceof GuardDenied) {<br>console.log(err.matched, "→", err.reason)<br>// → sql.denyDDL → DDL not allowed: drop

await safeQuery({ query: "SELECT id FROM users WHERE id = $1" })<br>// runs the handler

Test rules without side effects:

guard.simulate("db.query", { query: "DROP TABLE users" })<br>// → { decision: "deny", reason: "DDL not allowed: drop", matched: "sql.denyDDL", irreversible: true }

For the full guide (custom rules, framework shims, audit log, failure modes, the irreversible flag): see USAGE.md.

What's in the box

Adapters

Adapters parse the raw payload into a typed shape. Rules see ASTs, not strings.

Adapter<br>Payload<br>Parses into

sql.postgres / sql.mysql / sql.sqlite

{ query, params? }

kind, tables, hasWhere, ddlOp, dialect

http<br>{ url, method?, headers?, body? }<br>parsed URL with IPv4-mapped IPv6 normalization, lowercased header keys

shell

{ command } or { argv }

tokenized argv, metacharacter / pipe / redirect / substitution flags

fs<br>{ path, op? }<br>normalized absolute path, op classification

raw<br>anything<br>passthrough — for cross-adapter custom rules

Built-in rules

Rule<br>Blocks<br>Tags irreversible

rules.sql.denyDDL()

DROP, TRUNCATE, ALTER, CREATE, RENAME

yes

rules.sql.denyMutationWithoutWhere()

UPDATE / DELETE with no WHERE

yes

rules.sql.denyTables({ deny, allow })<br>Configured table denylist or allowlist (case-insensitive, schema-stripped)<br>yes for writes, no for SELECT

rules.http.denyHosts(list)<br>Host literals, IPv4/IPv6 CIDRs, *.wildcards

no

rules.http.allowHosts(list)<br>Anything not on the list<br>no

rules.http.SSRF_DEFAULTS<br>RFC1918, link-local, loopback, AWS metadata, *.internal, *.local

(constant)

rules.shell.denyCommands(list, opts?)<br>Banned commands (basename) plus metacharacter abuse<br>yes for rm/dd/mkfs/etc., configurable via destructive option

rules.fs.confineTo(roots, opts?)<br>Anything outside the configured roots<br>yes for write/delete, no for read/list

rules..custom({ on, when,...

rules query owthorize drop http model

Related Articles