A small macOS approval and credential boundary for agent-requested commands.
The root-owned Swift helper does not understand Supabase migrations, SQL, linking, deployment state, or provider workflows. The agent remains responsible for inspecting state and constructing the exact command. The guard displays that request, asks the device owner to approve it locally or from an enrolled phone, substitutes explicitly requested Keychain credentials internally, and executes the approved argument vector without a shell.
The guard must never interrupt a command it can establish as non-mutating. Read requests pass through without device-owner authentication; only commands that mutate state or whose effect cannot be safely classified retain the approval prompt. This applies broadly rather than by content or table: the same policy covers any Supabase project, table, view, content type, and read query.
For direct PostgreSQL access, the guard accepts one top-level read statement (SELECT, WITH, VALUES, TABLE, SHOW, or non-ANALYZE EXPLAIN) supplied through psql -c or psql --command. It accepts ordinary psql presentation, connection, and local-output options. It rejects only options that can inject or load uninspected SQL (-f/--file, -v/--set/--variable), multiple statements, and SQL that writes or takes locks.
For Supabase REST reads, use the dedicated curl header placeholders below. They expand inside the helper, never appear in shell arguments or command output, and work with approval-free GET/HEAD requests:
curl -H '{{service-role-apikey-header}}' \
-H '{{service-role-bearer-header}}' \
'{{project-url}}/rest/v1/content_entities?select=id'ShallNotPass is a deliberately narrow boundary for agent-requested access to a Supabase instance. Its intent is to keep an agent from receiving unmediated destructive capability: selected destructive operations are rejected outright, while mutating Supabase commands must be shown to—and approved by—the macOS device owner through system authentication (a password or Touch ID, when available).
The policy can be extended for a particular environment by adapting HardDenialPolicy: add forbidden executables, Supabase arguments, SQL patterns, or tightly defined read-only commands that match the environment's threat model. Treat every such change as a security-sensitive policy decision and review the resulting command surface carefully.
This project is intended as a small, inspectable example and source of inspiration for similar approval boundaries. It is not a definitive security implementation, a complete database policy engine, or a substitute for least-privilege credentials, backups, access controls, monitoring, and independent security review.
Requirements:
- macOS 13 or later;
- Xcode Command Line Tools (
xcode-select --install); - a Developer ID Application signing identity to install the protected helper;
- the Supabase CLI for the commands you plan to guard.
Clone the repository, then build the release helper:
git clone https://github.com/WorldBrain/ShallNotPass.git
cd ShallNotPass
swift build -c release
./bin/supabase-ops-guard helpThe unsigned build product is .build/release/supabase-ops-guard-helper. The wrapper in bin/ uses that local build while developing.
The installer requires a locally available Developer ID Application identity. List the available identities with:
security find-identity -v -p codesigningSet the identity to use, then install:
export SOG_CODESIGN_IDENTITY='Developer ID Application: Your Name (TEAMID)'
./install.shThe installer builds, signs, and verifies the helper, then uses sudo to install the signed helper at /usr/local/libexec/supabase-ops-guard/ and the command wrapper at /usr/local/bin/supabase-ops-guard. Enter the administrator password yourself in a trusted terminal; it is never passed to the installer.
Verify the installed command:
supabase-ops-guard helpTo remove it later, delete both installed paths from a trusted terminal:
sudo rm -f /usr/local/bin/supabase-ops-guard
sudo rm -rf /usr/local/libexec/supabase-ops-guardsupabase-ops-guard setup productionThe current setup stores a Supabase project URL, service-role key, and optional database URL in the macOS Keychain. Each item is restricted to the signed guard helper using macOS's application ACL compatibility API, so another command-line process cannot retrieve it directly. Apple has deprecated this older ACL API but does not offer an equivalent modern per-executable ACL for a standalone command-line helper. Secrets are never printed. When a profile already has a value, press Return to keep it; enter a value only to replace it.
Install and sign in to Tailscale on both the Mac and phone, and enable MagicDNS and HTTPS for the tailnet. Then run this once from a trusted terminal on the Mac:
supabase-ops-guard remote enroll productionThe guard performs local macOS device-owner authentication, restricts existing profile credentials to the signed helper, configures a private Tailscale Serve route on HTTPS port 8443, and starts a five-minute enrollment session. It prints REMOTE_APPROVAL_URL and a ready-to-post REMOTE_APPROVAL_MARKDOWN link. Create the passkey with Face ID there. The passkey private key stays in the phone's passkey provider; the Mac stores only its credential ID and public key.
The local approval service listens on 127.0.0.1:18787; Tailscale terminates HTTPS and forwards the private tailnet request to it. Override either address when needed:
supabase-ops-guard remote enroll production \
--origin 'https://your-mac.your-tailnet.ts.net:8443' \
--port 18787The origin is the exact HTTPS origin seen by the phone. Re-enrolling replaces the prior passkey. Inspect or disable the enrollment with:
supabase-ops-guard remote status production
supabase-ops-guard remote disable productionDisabling remote approval requires local macOS authentication. Tailscale's Serve route can be cleared separately with tailscale serve clear if it is no longer needed.
supabase-ops-guard exec production /path/to/project \
--job 'Apply the approved database migration to production' -- \
supabase db push --db-url '{{database-url}}'Every guarded request must include a concise, human-readable job description. After phone enrollment, every guarded write starts the macOS device-owner prompt and the phone approval page together. Before waiting, the guard emits REMOTE_APPROVAL_URL, a ready-to-copy REMOTE_APPROVAL_MARKDOWN link, REMOTE_APPROVAL_AGENT_INSTRUCTION, and the desktop state. Agents must post the Markdown value in the active chat immediately and keep polling the original process. The first successful approval wins: phone approval invalidates the macOS prompt, while desktop approval closes the local server and makes the phone request expire. The page shows the job, profile, working directory, exact argument vector, and request fingerprint. It attempts to start Face ID when opened and keeps an approval button visible for browsers that require a tap.
Agent runners must start guarded commands with live output and a retained process handle. In Codex shell calls, use a short initial yield (one second is recommended) and a TTY; if the first result contains only a session ID, poll that same session until the REMOTE_APPROVAL_MARKDOWN line appears. Never suppress guard output or replace the waiting process with a reconstructed command.
The WebAuthn challenge is derived from the complete request plus a cryptographic nonce. Approval is therefore single-use, expires after five minutes, and cannot authorize a modified command. Credential substitution and execution happen on the Mac only after the enrolled public key verifies the phone's signature.
Bind files or directories whose contents determine the operation with repeatable --bind options. The page displays a SHA-256 fingerprint for every bound input, and the guard recomputes all fingerprints after approval. Any added, removed, or modified file cancels execution:
supabase-ops-guard exec production /path/to/project \
--bind supabase/migrations \
--job 'Apply pending database migrations to production' -- \
supabase db push --db-url '{{database-url}}' --include-all --yesBound paths must be regular files or directories inside the working directory and may not contain symbolic links.
The job description is supplied by the requester and never overrides the exact command. Credential placeholders must occupy a complete argument; embedded or unknown placeholders are rejected.
The following Supabase read-only commands pass through without device-owner authentication:
supabase statussupabase migration listsupabase functions listsupabase projects listsupabase db diffsupabase inspect dbsupabase gen types
The guard allows the configured read-only Supabase commands, any curl GET or HEAD request, and generic top-level PostgreSQL reads through psql without a prompt. The psql path requires {{database-url}} and exactly one -c/--command statement. It accepts ordinary non-executing psql options and rejects only SQL-file execution and variable interpolation, multi-statements, utility, mutating, and locking SQL. For example:
supabase-ops-guard exec production /path/to/project \
--job 'Count saved content entities' -- \
psql -X '{{database-url}}' -c 'SELECT count(*) FROM public.content_entities'Other commands, including curl writes and commands whose HTTP method cannot be determined safely, retain the approval prompt.
Supported placeholders:
{{project-url}}{{service-role-key}}{{database-url}}{{service-role-apikey-header}}{{service-role-bearer-header}}
The helper uses Process with an argument vector rather than a shell, so shell operators and substitutions have no special meaning unless the explicitly approved executable is itself a shell.
The following requests are rejected before an authentication prompt and cannot be approved:
supabase db resetand other Supabase reset forms;- direct
DROP TABLE,DROP SCHEMA,DROP DATABASE, orTRUNCATESQL; - shells, language interpreters, and direct SQL clients other than the narrowly constrained read-only
psqlform above.
Database resets and table deletion must be performed manually through the Supabase web application.
The guard's policy is compiled into HardDenialPolicy in Sources/SupabaseOpsGuard/main.swift. Start by writing down the operation to prevent, the executable and arguments an agent could use to request it, and any alternate forms that could bypass a narrow match. Keep the policy specific: an overly broad rule can block legitimate operational work, while an incomplete rule can leave a bypass.
Choose the policy surface that matches the request:
forbiddenExecutablesrejects an executable before any approval prompt. Add shells, interpreters, database clients, or other tools that could hide an operation the approval screen should not permit.forbiddenSupabaseArgumentsrejects matching Supabase CLI arguments regardless of their order. Add a command argument here when that operation must never be approved through the guard.forbiddenSQLPatternsrejects destructive SQL forms after SQL comments are removed. Add a carefully scoped, case-insensitive regular expression when a direct SQL operation must never be approved.readOnlySupabaseCommandsdefines the small set of Supabase argument prefixes allowed without device-owner authentication. Add an entry only after establishing that the complete command family cannot mutate state or request a Keychain credential. Adding a prefix here removes the approval prompt for matching commands.
After changing policy, build the helper and inspect the behavior against a non-production project before installing it for real use:
swift build -c release
./bin/supabase-ops-guard helpWhen the changed policy is ready to install, sign and install the rebuilt helper using the normal process:
export SOG_CODESIGN_IDENTITY='Developer ID Application: Your Name (TEAMID)'
./install.shDo not rely on the guard alone to make a destructive action safe. Keep production credentials least-privileged, retain backups and recovery procedures, and have a person review the exact requested command before approving it.
Apart from the compiled HardDenialPolicy, the guard deliberately makes no judgment about whether a requested command is a valid migration, safe SQL, the right deployment operation, or properly ordered. That analysis belongs to the requesting agent and remains visible in the conversation. The guard provides only:
- an exact request display;
- pass-through for established reads, or local macOS / enrolled phone approval for mutations and unclassified commands;
- protected credential substitution;
- execution and exit-status reporting.