Agent guard
safegrd guard backs up a surface, waits until the snapshot is uploaded and
locked, and only then runs the command. A hook in your coding agent calls it before every
shell command that matches the destructive list, so a snapshot exists before an agent drops
a table or resets a database.
Run a command behind a snapshot
safegrd guard --surface app-primary -- psql "$DATABASE_URL" -c 'DROP TABLE sessions'
safegrd guard -- npx prisma migrate reset
The surface is --surface, the only surface in the config, or the config's
database_url (or SAFEGRD_DATABASE_URL) when it lists none. If the
backup fails, or the snapshot is not locked, the command does not run.
Undo what the command did
Guard prints the snapshot it took. To bring back a table the command dropped or emptied, restore that table from it. A dropped table is created as the snapshot defined it, with its sequences, constraints and indexes; an emptied one is loaded into the empty table. The database's other tables are left as they are.
safegrd restore --snapshot snap-20261005-120428-524927 --table public.sessions --target env:DATABASE_URL
For a command that changed more than a few tables, restore the whole snapshot into an empty database instead (Databases).
| Exit code | Meaning |
|---|---|
| the command's own | The snapshot is locked and the command ran. |
| 3 | Guard did not run the command: the backup failed or the snapshot is not locked. |
| 1 | A usage or configuration error. |
A snapshot counts as locked when it was written under Object Lock with a retention date in
the future. Storage on the host's own disk and a bucket with worm_mode: NONE
cannot lock, so guard refuses there unless you pass --allow-unlocked.
Everything guard prints goes to stderr. Stdout belongs to the command.
Reuse a recent snapshot
An agent often runs several destructive commands in a row. With --max-age, a
locked snapshot of the surface younger than that stands in for a new backup, and guard
says which one it is relying on. Without one it backs up as usual.
safegrd guard --max-age 30m -- psql "$DATABASE_URL" -c 'TRUNCATE sessions'
--check-only takes no backup at all: the command is refused (exit 3) unless a
locked snapshot younger than --max-age exists. Use it where the surface is backed
up by its daemon or on SafeGrd, and the host the agent runs on should only check. A snapshot
counts when it is this surface's, completed, and still under Object Lock, as
safegrd list --json reports it.
safegrd guard --max-age 1h --check-only -- terraform destroy
The destructive list
safegrd guard --list prints the rules. safegrd guard --matches
"<command>" exits 0 when a command matches and 1 when it does not, and takes no
backup. The whole command line is matched, so SQL inside quotes counts.
| Rule | Matches |
|---|---|
| SQL DROP | DROP TABLE, DATABASE, SCHEMA, VIEW, INDEX, COLUMN and the other object types |
| SQL TRUNCATE | TRUNCATE users, TRUNCATE TABLE orders |
| SQL DELETE without WHERE | DELETE FROM users; |
| dropdb | dropdb app |
| terraform destroy | terraform destroy, terraform apply -destroy, and tofu |
| prisma | prisma migrate reset, prisma db push --force-reset |
| supabase db reset | supabase db reset |
| rails | db:drop, db:reset, db:schema:load, db:purge |
| django | manage.py flush, manage.py reset_db |
A command that only mentions one of these, such as grep "DROP TABLE" schema.sql,
also matches. It costs one extra snapshot.
Hooks
With --hook, guard reads the hook's JSON on stdin and answers in the tool's
own format. A command that does not match is let through with no backup. One that matches
is backed up first. If that backup fails or is not locked, the command is blocked, and the
agent is told why. Install the CLI and enrol the host first, then add the hook to the
project, naming the surface the agent works on. Set the timeout high enough for one backup
of that surface. --max-age and --check-only work in a hook too:
safegrd guard --hook claude-code --surface app-primary --max-age 30m backs up at
most once every 30 minutes however many destructive commands the agent runs.
Claude Code
In .claude/settings.json. With the
Claude Code plugin installed, ask Claude to set up the
SafeGrd guard hook and it adds this for you.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "safegrd guard --hook claude-code --surface app-primary",
"timeout": 600
}
]
}
]
}
}
Cursor
In .cursor/hooks.json:
{
"version": 1,
"hooks": {
"beforeShellExecution": [
{
"command": "safegrd guard --hook cursor --surface app-primary",
"timeout": 600
}
]
}
}
Codex
In .codex/hooks.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "safegrd guard --hook codex --surface app-primary",
"timeout": 600
}
]
}
]
}
}
Check the setup
safegrd doctor --agent-proof
It checks what an agent on this host could reach, and prints the fix for each check that
fails. --json gives a result you can share.
| Check | Passes when |
|---|---|
| Bucket in another account | The bucket and the database are with different providers, or the bucket is SafeGrd's hosted storage. Where both are with one provider the config cannot tell the accounts apart, and the check says so. |
| Storage key cannot delete | The bucket refuses this host's key a delete. The check asks about a key no snapshot uses. |
| Object Lock in compliance mode | worm_mode is COMPLIANCE and the bucket has Object Lock enabled. |
| Drill passed in 7 days | The remote server has a passed Fire Drill for each surface from the last 7 days. |
| Agent uses a personal token | The agent configurations on this host give SafeGrd a personal access token (sg_pat_), or use the local safegrd mcp. |