Docs / Agent guard

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 codeMeaning
the command's ownThe snapshot is locked and the command ran.
3Guard did not run the command: the backup failed or the snapshot is not locked.
1A 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.

RuleMatches
SQL DROPDROP TABLE, DATABASE, SCHEMA, VIEW, INDEX, COLUMN and the other object types
SQL TRUNCATETRUNCATE users, TRUNCATE TABLE orders
SQL DELETE without WHEREDELETE FROM users;
dropdbdropdb app
terraform destroyterraform destroy, terraform apply -destroy, and tofu
prismaprisma migrate reset, prisma db push --force-reset
supabase db resetsupabase db reset
railsdb:drop, db:reset, db:schema:load, db:purge
djangomanage.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.

CheckPasses when
Bucket in another accountThe 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 deleteThe bucket refuses this host's key a delete. The check asks about a key no snapshot uses.
Object Lock in compliance modeworm_mode is COMPLIANCE and the bucket has Object Lock enabled.
Drill passed in 7 daysThe remote server has a passed Fire Drill for each surface from the last 7 days.
Agent uses a personal tokenThe agent configurations on this host give SafeGrd a personal access token (sg_pat_), or use the local safegrd mcp.