DOCUMENTATION

Guard every tool call your agent makes.

tack-guard is runtime risk scoring for AI agent tool calls: it scores every tool call 0 to 1 against a per-agent baseline and contains escalations inline, in under 1ms, with no LLM and zero dependencies. This page is the full reference: install, the API, the eight signals, and the four attack patterns.

Last updated June 16, 2026

Install

tack-guard ships as a single zero-dependency package on npm. It is written in TypeScript, ships both ESM and CommonJS builds with bundled types, and runs on Node 18 or newer.

  • Zero runtime dependencies
  • ~17KB bundle
  • evaluate() under 1ms
  • ESM + CJS, typed
  • MIT licensed

Quick start

Create a guard once, then evaluate each tool call before you run it. Every call returns a graded verdict; you skip the tool whenever result.blocked is true and hand the summary back to the model.

agent.ts
import { createGuard } from '@tacksec/guard'

const guard = createGuard()

// Inside your agent's tool-execution pipeline:
const result = guard.evaluate({
  action: 'write',
  target: 'user_db.credentials',
})

if (result.blocked) {
  // Skip the tool call and return result.summary to the model.
} else {
  // Run the tool.
}

A blocked, escalating call returns a fully explained result:

GuardResult
{
  verdict: 'quarantine',
  score: 0.57,
  severity: 'CRITICAL',
  pattern: 'credential-creep',
  signals: { escalation_trajectory: true, new_write_access: true, sensitive_write: true }, // plus the other 5 signals, all false
  evidence: [
    'access to sensitive target: user_db.credentials',
    'trajectory: read-only -> mutate -> sensitive/admin over 3d',
  ],
  summary: 'Agent escalated from read-only to sensitive access. Credential-creep detected.',
  quarantined: true,
  blocked: true,
}

How scoring works

Two complementary layers run on every call. The score is the weighted sum of the signals that fired, clamped to 0 to 1: it grades with the evidence, it is not a hardcoded constant.

Stateless signatures

Memory-independent. They catch blatant single-shot attacks, a bulk credential export or a destructive wipe, on call one with no history. Actions are canonicalized, so Exfiltrate, export_all and drop_table all match.

Behavioral scoring

Memory-dependent. It tracks a per-agent baseline and scores deviations. This is what catches slow credential-creep, read-only for days then a first write then a reach into sensitive data, where no single call is damning.

The eight weighted signals

Each signal contributes its weight when it fires. Override any of them with the weights config.

SignalWeightWhat it catches
escalation_trajectory 0.25 Read-only baseline violated, scope grew to sensitive or admin.
sensitive_write 0.22 Access to a sensitive target (credentials, PII).
sensitive_egress 0.30 Sensitive data then sent to an untrusted destination. Opt-in via egressTargets.
volume_spike 0.15 Call volume at or above 3x the baseline.
rapid_burst 0.15 Many calls in a short window (an automated burst). Temporal.
new_write_access 0.10 First mutation after a read-only baseline.
after_hours 0.08 Activity outside business hours.
admin_attempt 0.07 Admin access, or an admin target touched.

The four attack patterns

PatternSeverityShape
credential-creep CRITICAL Slow escalation from read-only into sensitive or admin access. Reports the escalation time span.
data-exfiltration CRITICAL Sensitive data accessed, then sent to a destination outside egressTargets. Opt-in.
smash-and-grab CRITICAL / HIGH A loud single-shot signature hit, or a rapid automated burst.
behavioral-drift HIGH / MEDIUM Other anomalies that push the score over the detection threshold.

Map your tool calls

The one catch with { action, target } is mapping your real tool calls onto it. tack-guard does that for you, so synonyms and casing cannot slip past a literal string check.

Infer from a provider tool call. OpenAI and Anthropic function-calling, the Vercel AI SDK, and MCP all emit { name, arguments }. inferToolCall maps the tool name to an action and the arguments to a target.

loop.ts
import { createGuard, inferToolCall } from '@tacksec/guard'

const guard = createGuard()

for (const call of response.tool_calls) {
  const verdict = guard.evaluate(inferToolCall(call))
  if (verdict.blocked) continue // Skip the tool, return the refusal to the model.
  runTool(call)
}

Or wrap a tool once and forget about it. A blocked call does not run: it throws GuardBlockedError, or calls your onBlocked handler.

wrap.ts
const safeQuery = guard.wrapTool(db.query, {
  action: 'read',
  target: (sql) => (sql.includes('credentials') ? 'user_db.credentials' : 'db'),
})

await safeQuery('SELECT ...') // Throws GuardBlockedError if the guard blocks it.

Under the hood, canonicalizeAction folds any verb or tool name into six classes: read, write, admin, exfiltrate, destroy, and other. It is deliberately conservative, so an email or upload agent is not quarantined for doing its job.

API reference

createGuard(config?)

Creates a guard. Every option is optional; the defaults are safe for most agents.

config.ts
const guard = createGuard({
  sensitiveTargets: ['payments', 'api_keys', 'credentials'], // string[] or predicate
  egressTargets: ['mycorp.com'],   // opt-in allowlist of trusted destinations
  businessHours: [9, 18],          // [start, end), default [9, 18)
  mode: 'warn',                    // 'warn' (default, advisory) | 'enforce' | 'score-only'
  baselineMin: 5,                  // calls before a baseline is established
  detectionThreshold: 0.5,         // score below this is no detection
  weights: { rapid_burst: 0.2 },   // partial override of signal weights
})
OptionTypeDescription
sensitiveTargets string[] | (target) => boolean Targets treated as sensitive. Default: unambiguous secrets (credential, secret, password, api_key, apikey, private_key, token, ssn, sensitive), narrow on purpose; pass your own to widen.
egressTargets string[] | (dest) => boolean Allowlist of trusted egress destinations. Opt-in: dormant if unset, so no egress false positives by default.
businessHours [number, number] Business-hours range [start, end) for the after-hours signal. Default [9, 18).
weights Partial<Record<SignalName, number>> Partial override of signal weights. Unspecified signals keep their default.
baselineMin number Calls observed before a baseline is established. Default 5.
detectionThreshold number (0-1) Score below this is treated as no detection. Default 0.5.
mode 'warn' | 'enforce' | 'score-only' Verdict policy. Default 'warn' (advisory, observe before you block).

guard.evaluate(call)

Scores a single tool call synchronously and returns a GuardResult. This is the hot path: no model, no network.

evaluate.ts
const result = guard.evaluate({
  action: 'update_row',        // any string, canonicalized internally
  target: 'user_db.accounts',  // what it is touching
  agentId: 'sales-bot',        // optional, default 'default'
  hour: 14,                    // optional, default current hour
  at: 1718000000000,           // optional epoch ms, default now
})

Input: the ToolCall fields.

FieldTypeDescription
action string Required. What the agent is doing. Any string, canonicalized internally.
target string Required. What it is touching, e.g. user_db.accounts.
agentId string Optional. Agent identifier for multi-agent setups. Default 'default'.
hour number (0-23) Optional. Hour of day for the after-hours signal. Default current hour.
at number Optional. Event time as epoch ms for the temporal signals. Default now.
metadata Record<string, unknown> Optional. Arbitrary metadata attached to the event.

Returns: a GuardResult.

FieldTypeDescription
verdict 'allow' | 'warn' | 'deny' | 'quarantine' Final decision for this call.
score number (0-1) Threat confidence. Higher is more dangerous.
severity 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' Severity classification.
pattern Pattern Detected attack pattern, or 'none'.
signals Record<SignalName, boolean> Which behavioral signals fired (may be all false on a pure signature hit).
evidence string[] Human-readable lines explaining why the signals fired.
summary string One-line summary of the verdict.
quarantined boolean Whether the agent is currently contained (persists across calls).
blocked boolean Whether this specific call was blocked.

guard.evaluateAsync(call)

Same as evaluate(), but awaits an asynchronous MemoryClient (Redis, a network DB, a cloud store). Scoring stays synchronous and local; only the memory I/O is awaited. evaluate() throws on an async client rather than silently degrading.

guard.wrapTool(fn, opts)

Wraps a tool so every call is evaluated first; blocked calls do not run. action and target can be static or functions of the args. It is synchronous, so a blocked call throws synchronously: use try / catch, not .catch(). Pass onBlocked to handle a block without throwing.

inferToolCall(rawToolCall, overrides?)

Maps a provider { name, arguments } tool call to a ToolCall. The action comes from the tool name (canonicalized), the target from the arguments. Override either explicitly.

canonicalizeAction(raw)

Folds any verb or tool name into one of six canonical classes: read, write, admin, exfiltrate, destroy, other. Case and synonym aware.

guard.connect(apiKey) roadmap

Persistent, cross-session baselines via one line. Not yet implemented: it throws until Tack Cloud ships. The local guard works fully standalone today.

guard.reset() · setMemory() · getAgentState()

reset(agentId?) clears state, setMemory(client) plugs in a custom backend, and getAgentState(agentId) inspects an agent.

Exported constants and types

For advanced tuning, tack-guard exports WEIGHTS, BASELINE_MIN, DETECTION_THRESHOLD and the SignalName type, plus the Verdict, Severity, Pattern, ToolCall, GuardResult, GuardConfig and MemoryClient types.

Modes

mode sets the verdict policy. Start in score-only or warn to observe your traffic, then switch to enforce once the numbers look right for you.

ModeBehavior
warn Default (advisory). Reports threats and returns a 'warn' verdict, but never blocks: observe first, then enforce.
enforce Quarantines on CRITICAL or a signature hit, and blocks risky calls while the agent is quarantined.
score-only Always allows. Returns the score and signals only, never blocks.

Custom memory

The default in-memory client loses baselines on restart, by design. Implement the four-method MemoryClient interface to back it with your own store. Synchronous backends (an in-process cache, better-sqlite3) use evaluate(); async backends (Redis, a network DB) use evaluateAsync().

redis-memory.ts
import { createGuard, type MemoryClient } from '@tacksec/guard'

class RedisMemory implements MemoryClient {
  async push(agentId, event) { /* ... */ }
  async getEvents(agentId) { /* returns Promise<StoredEvent[]> */ }
  async clear(agentId) { /* ... */ }
  async clearAll() { /* ... */ }
}

const guard = createGuard()
guard.setMemory(new RedisMemory())
await guard.evaluateAsync({ action: 'write', target: 'users' })

Persistent, cross-session baselines that survive restarts are Tack Cloud's job, exposed through guard.connect().

Reference: verdicts and severity

Every result carries a verdict and a severity.

VerdictMeaning
allow Let the call through.
warn Let it through but flag it. The only non-blocking detection verdict.
deny Block this single call without quarantining. Reserved: currently every block path also quarantines.
quarantine The agent is contained. This and future risky calls are blocked.

Severity is one of LOW, MEDIUM, HIGH, CRITICAL, scaled with the score.

Limitations and threat model

tack-guard advises; your pipeline enforces. It returns a verdict, and you must actually skip the blocked tool call. It is a fast heuristic layer, not a complete defense. The honest limits, by design:

  • Sensitivity is name-based. Targets are matched by substring (or your sensitiveTargets), so a destructive op on an innocuously named table can be missed, and a benign write to a user_db.* table can over-flag. Pass a predicate for precision.
  • Behavioral detection needs a read-only baseline. An agent that mutates from call one has no normal to deviate from; it relies on the signature layer.
  • Single session only. Baselines live in memory and reset on restart. Multi-day, cross-session correlation is Tack Cloud.
  • Not a prompt-injection filter. It scores behavior, what the agent did, not prompt content. Use it alongside input and output guards.

FAQ

Does tack-guard call an LLM?
No. Scoring is deterministic heuristics plus a per-agent baseline. There is no model and no network call in the hot path, so the verdict is reproducible and explainable.
How fast is it?
Inline and synchronous, under 1 millisecond. tack-guard has zero runtime dependencies and a bundle of roughly 17KB.
Does it stop prompt injection?
Not directly. tack-guard scores the action an agent takes, not the prompt text. Run it alongside an input or output prompt guard for defense in depth.
Is it free?
Yes. tack-guard is MIT licensed and runs locally. Persistent, cross-session baselines and dashboards are the paid Tack Cloud tier.
Does it need training data?
No. It builds each agent's baseline live from the first few calls (baselineMin, default 5). The stateless signature layer still catches blatant attacks on call one.

Next steps