Agents Arm at scale
bulk tripwires

Arm one tripwire per row. In one CLI command.

A single agent reading one wrong row can exfiltrate the whole customer database. The defence is per-row, not per-bundle. The deny-sh tripwires arm-bulk command arms up to a million decoys in one call, returns a CSV you write back to your DB, and pages on-call within ~5 seconds the first time any of them shows up at /api/decrypt.

The shape

The bulk endpoint, the SDK helper, the CLI: same primitive, three surfaces. Generate a decoy that looks like a real credential of the same type, hash it, register the hash, hand back the decoy. Store the decoy alongside the real row. The real value stays where it normally lives.

Worked example: 250k Stripe rows

You run a B2B SaaS. Every customer row in your DB has a real Stripe secret in stripe_key_real. You add a stripe_key_decoy column. Every agent that reads a customer row sees both columns; an agent that exfiltrates the row exfiltrates the decoy too. The decoy looks identical to a live key (sk_live_ prefix, 107 chars, same charset). The first time it ever shows up at /api/decrypt anywhere on the internet, you get paged.

1. Get a tenant API key

Tripwires need at least the Pro tier ($199/mo, 10k cap). At 250k rows you need Agents Starter ($299/mo, 100k cap) twice over or Agents Infrastructure ($999/mo, 1M cap) once. The 1M cap is the natural fit for any DB north of 100k rows; the cost is dominated by your DB column writes, not deny.sh.

2. Arm the tripwires

One command. Writes a CSV with every armed decoy + its tripwire id:

# Install the CLI (one-time)
npm install -g deny-sh

# Arm 250k stripe-live-key decoys, save the mapping
DENY_API_KEY=cs_your_tenant_key \
  deny-sh tripwires arm-bulk \
    --type stripe-live-key \
    --count 250000 \
    --label-prefix prod-stripe \
    --out armed.csv

# Sustained ~500/sec; 250k rows in about 8 minutes.
# Each row in armed.csv:
#   label,decoy_value,tripwire_id,hash_suffix
#   prod-stripe-0000001,sk_live_decoyAbc123...,42891,7b9c4e5f3a2d

3. Write the decoys back to your DB

Whatever import tool you already use: psql \copy, mysql LOAD DATA, dynamodb batch-write-item, a 20-line Python script. The pattern is one column added, one bulk update, done. The tripwire_id column stays on your side as a reference; you can pass it to deny-sh tripwires test <id> if you ever want to fire one synthetically to test your alert pipeline.

4. Wire the alert fan-out

Tripwire events route through /dashboard/webhooks, same as every other audit event. Configure Datadog API key, PagerDuty integration key, or Slack webhook URL once. The event shape is decoy.tripwire.triggered with the matched label, the tenant id, the timestamp, the request IP, and the request user-agent. Within ~5 seconds of the decoy showing up at /api/decrypt, on-call gets paged.

What lands in Slack

The webhook adapter ships a structured payload; the Slack adapter renders it as a block-kit message. Stripped of formatting, the body is:

🚨 Decoy tripwire triggered
label: prod-stripe-0148732
tenant: acme-prod
fired_at: 2026-07-04T14:22:13Z
remote_ip: 198.51.100.42
ua: python-requests/2.31.0
audit_chain: ✓ signed, receipt RFC-3161
investigate: https://deny.sh/dashboard/decoy-alerts/42891

The decrypt response itself returns a 200 with a plausible-looking failure (same shape as a wrong-password attempt). The attacker can't distinguish a tripped wire from a normal failed decrypt; you can.

From real DB rows (the arm-from-csv path)

If you'd rather drive arming from an existing export instead of generating fresh decoys with sequential labels, point the CLI at a CSV. It reads one row at a time, arms a decoy for each, writes the result back next to the source id:

# real-rows.csv (one column: customer_id)
# 1042
# 1043
# ...

DENY_API_KEY=cs_your_tenant_key \
  deny-sh tripwires arm-from-csv \
    --type aws-access-key \
    --in real-rows.csv \
    --id-col 0 \
    --skip-header true \
    --label-template "cust-{id}-aws" \
    --out armed.csv

# Output rows match input rows by id, so a Python or psql join
# writes the decoys straight back into the customer table.

Labels carry the customer id, which means a tripped wire tells you exactly which row leaked. The audit-chain receipt is enough evidence on its own; the structured webhook payload gives you the routing target inside your stack.

Tiers and caps

Tripwires are NOT available on Free or Dev tiers. The cap is per-tenant, soft-enforced at the bulk endpoint:

TierTripwire capPrice
Freen/a$0
Devn/a$99/mo
Pro10,000$199/mo
Agents Starter100,000$299/mo
Agents Infrastructure1,000,000$999/mo
Enterprise10,000,000from $50k/yr

Counts are per-tenant. If you run a multi-tenant platform with one deny.sh tenant per end-customer, each end-customer gets their own cap.

Why this is the alert layer for AI agents

A general-purpose AI agent with database read access, a fetch tool, and a prompt-injection-friendly input surface can be told to read a customer row and POST it somewhere. Conventional database access logs see the row read; they don't see anything wrong because the agent is supposed to read rows. The decoy column makes the leak observable. The first time the decoy bytes hit /api/decrypt (because the attacker did the natural thing and tried to use the credentials they exfiltrated), you get paged.

This works because the decoy is shape-correct (passes whatever offline validation the attacker runs: prefix, length, charset, checksum) and indistinguishable from a real key without trying it. Attackers don't know which column is real and which is decoy. Trying both is the most efficient strategy, which is exactly what trips the wire.

Privacy boundary

Only the SHA-256 hash and the label cross the network. The decoy bytes never leave your machine; the real bytes never enter deny.sh's infrastructure. The webhook payload contains the label you chose, not the decoy value.

Where to go next