Your agent can only do what you said it could.

Thskyshield gives every agent run a declared boundary — how many steps it may take, which tools it may call, and whether it may write. The check happens before the call fires. Three SDK calls. Any loop.

$ npx @thsky-21/thskyshield-demo

No signup. No API key. No network. About four seconds.

Works with LangGraph · CrewAI · OpenAI Agents SDK · any framework

$ npx @thsky-21/thskyshield-demo
Thskyshield — runaway agent demo
An agent gets stuck in a retry loop. Watch it get killed.
Loop gate 19 identical prompts
Iteration limit 50
Model gpt-4o
step 01 allowed █░░░░░░░░░░░ 5% of loop gate
step 02 allowed █░░░░░░░░░░░ 11% of loop gate
↳ loop detector: identical prompt ×2 of 19
⋯ steps 03–18 elided
step 19 allowed ████████████ 100% of loop gate
↳ loop detector: identical prompt ×19 of 19
KILLED step 20 — loop threshold reached
Steps allowed 19 of 50
Killed by loop

Real output, verbatim — this is what the command above prints.

Why a boundary

A stuck agent doesn't crash, and an off-track agent doesn't ask.

The stuck shape

A stuck agent doesn't crash.

It retries. It re-asks. Every individual call returns a clean 200 — your monitoring shows a healthy service, because nothing ever throws. The loop is the failure, and a loop has no stack trace. By the time a human notices the bill, the run has been repeating itself for however long nobody was watching.

The off-track shape

An off-track agent doesn't ask.

It hits an obstacle — a permission it doesn't have, a file it can't write, a step that keeps failing — and it does what an agent does: it looks for a way through. Sometimes that way through is reasonable. Sometimes it's a tool call nobody scoped it for, reaching a system nobody meant it to reach. It doesn't pause to check, because nothing told it where the edge was.

The boundary

Five things every run declares.

Three are contained the moment you switch enforce on. Two are measured from day one, and enforced when you decide they should be.

Three things it cannot do

Tools it may call

A run declares its tool allowlist up front. A call to anything outside it is denied before it fires — killed_scope_tool.

What it may write

Every step declares how destructive it is — read-only, reversible, or destructive. A step over the run's ceiling is denied — killed_scope_mutation.

The same prompt again

The same prompt fingerprint repeated past the loop threshold is denied before the next call — killed_loop.

Two things you'll see first

Wall clock

A run that's been open too long is denied on its next step — killed_timeout.

Step count

A hard ceiling on how many steps a run may take, whatever each one does — killed_iterations.

We don't kill a run mid-task unless you tell us to. You'll see what would have happened first.

How it works

Three calls. That's the integration.

Wrap your agent loop with beginRun, beforeStep, and afterStep. One atomic round-trip, under 10ms, prompts hashed in your own process, fails open at every layer. There is no agent to run and nothing to host.

01
shield.beginRun()
Declare the boundary

Set a step cap, a timeout, a loop threshold, which tools may be called, and how destructive a write may be.

02
run.beforeStep()
The check, before the call fires

One round-trip checks every boundary — tools, writes, loop, steps, time — before your API call fires.

03
run.afterStep() + run.end()
Close the step

afterStep closes the step and records how it ended. end() finalises the run. What the boundary decided, and where, is on the record either way.

agent.ts
import { Thskyshield, ShieldKilledError } from '@thsky-21/thskyshield'

const shield = new Thskyshield({ siteId, apiKey })

const run = await shield.beginRun({
  iterationLimit:   30,
  loopThreshold:    5,
  allowedTools:     ['search', 'read_file'],   // the only tools this run may call
  maxMutationClass: 'reversible',              // no destructive writes
})

try {
  while (!done) {
    const { requestId } = await run.beforeStep({
      stepType:      'tool',
      toolName:      'search',
      mutationClass: 'read_only',
      promptInput:   currentPrompt,
    })

    await callYourTool(currentPrompt)

    await run.afterStep({ requestId })
  }
} catch (e) {
  if (e instanceof ShieldKilledError) {
    // e.reason: 'killed_scope_tool' | 'killed_scope_mutation' | 'killed_loop'
    //         | 'killed_iterations' | 'killed_timeout'
    console.log(`Agent stopped: ${e.reason}`)
  }
} finally {
  await run.end()
}
Watch first. Then enforce.

It watches before it touches anything.

Handing a kill switch to something you installed this morning is a lot to ask, so we don't ask for it. Every new project starts in observe mode.

You get the report either way. Enforcement is the part you turn on once you have read it — and there is nothing to undo if you never do.

  • Observe mode runs every check and stops nothing. Your agents behave exactly as they do today.
  • Each run records the decision it would have made — which boundary, and where. No card, nothing blocked.
  • Flip a project to enforce when you're ready. The switch applies to the next run, never one already in flight.
What enforce stops — a run ends on whichever limit trips first
killed_scope_tool
Tool not on the list
toolName ∉ allowedTools
killed_scope_mutation
Write too destructive
mutationClass > maxMutationClass
killed_loop
Loop detected
same prompt ≥ loop_threshold
killed_timeout
Timed out
elapsed > timeout_seconds
killed_iterations
Max steps hit
iter ≥ iteration_limit
Honesty

We'd rather tell you what we don't know.

A boundary that fails silently isn't a boundary — it's a guess you stopped checking. Every number and every verdict on this product is built to be wrong in only one direction: understated, never inflated.

  • When we can't reach a verdict, your agent runs anyway — and we say so. Every step carries a reason: a real decision, or exactly which part of our system was down when it went through unchecked.
Where the boundary lives

Policy that outlives the process.

The boundary isn't a constant in your code. It's a setting on your project, read fresh at the start of every run. Change it and the next run picks it up — no redeploy, no restart, no code review.

Every agent your team points at that project inherits the same boundary. One place declares it. Every run enforces the one that was declared, not whatever the code happened to ship with.

Your code
shield.beginRun({ ... })
Read at run-begin

Mode, allowlist, and mutation ceiling — resolved from your project's current settings, not baked into a deploy.

A change you make today

Applies to the next run your agent starts. Runs already in flight finish under the boundary they started with.

Pricing

Watch for free. Talk to us to enforce.

Free
$0
Forever
  • Every decision recorded — the full counterfactual
  • Nothing blocked
  • Unlimited history — nothing expires
  • No card required
  • —Enforce mode
Get started
Enforce
Enforce
Everything in Free, plus:
  • All five boundaries enforced — tools, writes, loop, steps, time
  • Denies the call before it fires, not after
  • Priority support, direct to the founder
Talk to us →

Enforce is priced per team while we're early — tell us what you're running and we'll walk you through it.

Things people ask us.

Give it a boundary it can't cross.

Free to watch, no card. Or watch it stop an agent at its boundary first — one command, nothing to install.

$ npx @thsky-21/thskyshield-demo