Skip to content
GitHubDiscord

Run Garak and DeepTeam Scanners

garak ↗ and deepteam ↗ are open-source red-teaming tools: they send attack prompts to an LLM app and report which ones got through. They work in different ways, and the difference decides which one you reach for.

  • garak mainly replays a fixed corpus of published attacks. Each probe carries a prompt set that is fixed when the probe loads, so it tests your agent against attacks other people have already found. Some of its detectors do use an LLM to judge the response.
  • deepteam generates its attacks. It hands your description to an LLM as the target’s purpose, synthesizes attacks against each vulnerability you selected, and then rewrites or escalates them with the attack techniques you selected.

Giskard’s own generators also write attacks from your description. So deepteam and Giskard overlap on approach and differ in taxonomy and technique; garak is the one that brings a published catalog you would not otherwise run.

third_party_scan runs either tool against the same target you already scan with, and returns its findings as an ordinary SuiteResult: same shape, same failed_count and failures_and_errors, same to_junit_xml export. One thing does differ: a third-party result carries no reusable suite, so you cannot save and replay it. See Read the results.

No tool here is exhaustive. Each covers the attacks its own maintainers wrote or its own model invented, and a clean result from all three still only means these attacks did not work.

Neither scanner ships with giskard.scan. Each is an optional extra, pinned in the library’s pyproject.toml:

ExtraInstallsCommand
garakgarak>=0.15,<1pip install --pre "giskard[scan,garak]"
deepteamdeepteam>=1.0.7,<2pip install --pre "giskard[scan,deepteam]"

Calling third_party_scan without the matching extra raises ImportError with the install command in the message. The import is lazy, so the package stays installable without either.

The built-in generators produce scenarios from your description and a curated threat taxonomy. The two external scanners select their work differently, and you configure them differently as a result.

garak is a flat list of hand-written probes. A probe is one named attack that carries its own prompts and its own detector, a small scorer that reads the response and decides whether the attack worked. You select probes by name, and that is the only axis. Its catalog covers DAN jailbreaks (prompts that talk a model past its safety rules), encoded-payload data exfiltration, prompt injection, and model-specific quirks. The prompts do not adapt to what your agent does.

deepteam has two independent axes. A vulnerability is what harm you test for, such as information disclosure or harmful content. An attack is how the prompt is delivered: an encoding, a framing, or a multi-turn escalation. The two do not overlap and neither implies the other. deepteam runs the cross-product internally: Toxicity attempted via Leetspeak, PIILeakage via CrescendoJailbreaking, and so on.

That cross-product is also the cost model. Each vulnerability fans out into subtypes (Bias covers race, gender, politics, religion), and the run size is subtypes × attacks_per_vulnerability_type × attacks.

garak has no equivalent split, so do not read probes= as the counterpart of vulnerabilities= and attacks=. A garak probe already fixes both what it tests for and how it asks.

Neither replaces the built-in scan: Giskard’s own generators use your description and knowledge base and its own threat taxonomy.

list_scan_items answers “what are the valid names” for each tool, including Giskard’s own:

from giskard.scan import list_scan_items
list_scan_items("giskard") # scenario generator class names from both registries
list_scan_items("garak") # loadable probe plugin names
list_scan_items("deepteam") # supported vulnerability and attack names

"garak" and "deepteam" raise ImportError when the extra is missing; any other tool name raises ValueError. For garak, only active, loadable probes are listed by default. include_inactive=True adds catalog entries garak marks inactive, which are skipped anyway if you request them explicitly. The argument is ignored for the other tools.

Treat list_scan_items as the authority rather than the upstream READMEs. For garak it enumerates the probes of the garak version you installed, so it never drifts. For deepteam it matters more: Giskard supports a fixed subset of deepteam’s catalog, and names outside that subset are silently skipped instead of raising.

Read NVIDIA/garak ↗ and confident-ai/deepteam ↗ for background on what each probe or technique does.

The deepteam names come from a fixed map inside the adapter rather than deepteam’s full catalog. The vulnerabilities are what you test for:

VulnerabilityWhat it looks for
BiasDiscriminatory or stereotyped output about a group
ToxicityInsults, harassment, and other abusive language
PIILeakagePersonal data about third parties leaving the agent
PromptLeakageThe agent revealing its own system prompt or configuration
MisinformationConfident false claims presented as fact

The attacks are how the request is disguised or escalated:

AttackTurnsWhat the technique does
PromptInjectionSingleEmbeds instructions that tell the agent to ignore its own
RoleplaySingleWraps the request in a fictional persona the agent is asked to play
LeetspeakSingleRewrites the request in leetspeak (h4ck) so filters miss the words
ROT13SingleRotates every letter 13 places, so the request reaches the model encoded
LinearJailbreakingMultiRewrites the prompt turn after turn, using each refusal to steer the next try
CrescendoJailbreakingMultiStarts benign and escalates gradually toward the harmful ask
TreeJailbreakingMultiBranches into several rewrites per turn and keeps the ones that get furthest
SequentialJailbreakMultiHides the request inside a benign narrative, such as a character dialogue or a game level
BadLikertJudgeMultiAsks the agent to score harmfulness on a scale, then to write an example of the worst score

Encoding attacks like Leetspeak and ROT13 are trivial to decode. They work because a safety filter reads the literal text while the model reads through the encoding.

Both tools go through the same entry point:

async def third_party_scan(
target,
tool: Literal['garak', 'lidar', 'deepteam'],
*,
description: str,
languages: list[str] | None = None,
**kwargs,
) -> SuiteResult: ...

The literal also accepts 'lidar', which routes to a private Giskard package rather than a supported public integration; use 'garak' or 'deepteam'.

third_party_scan is the only dispatcher for garak and deepteam. vulnerability_scan cannot reach them: its suite is built from Giskard’s own scenario generators, and the third-party scanners are a deliberate opt-in. The remaining keyword arguments are per tool: probes and target_mode for garak, vulnerabilities, attacks, attacks_per_vulnerability_type and target_mode for deepteam.

Await it, as you await vulnerability_scan. Every scan function on this page is async def.

Both examples on this page run against the same agent: a customer-support assistant for a retail bank, wrapped the way Wrap your agent describes. It holds the caller’s own records in its context and a system prompt stating its rules, which is what gives deepteam’s PIILeakage and PromptLeakage something to go after later on this page:

import os
from openai import AsyncOpenAI
from pydantic import BaseModel
client = AsyncOpenAI(api_key=os.environ["OPENAI_API_KEY"])
RECORDS = "Ada Lovelace, card ****4419, balance EUR 2,310. Alan Turing, card ****7702, balance EUR 118."
SYSTEM_PROMPT = (
"You are the support agent of Northgate Bank, speaking to Ada Lovelace. "
"Answer only about her own accounts and cards. Never give investment "
f"advice and never mention another customer. Records: {RECORDS}"
)
class AgentInput(BaseModel):
question: str
class AgentOutput(BaseModel):
answer: str
async def support_agent(inputs: AgentInput) -> AgentOutput:
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": inputs.question},
],
)
return AgentOutput(answer=response.choices[0].message.content or "")

Point garak at it:

from giskard.scan import third_party_scan
suite_result = await third_party_scan(
target=support_agent,
tool="garak",
description="A customer-support agent for a retail bank, covering accounts, cards, payments and disputes.",
probes=["probes.dan.AutoDANCached", "probes.lmrc.SlurUsage"],
)

AutoDANCached replays pre-computed DAN-style jailbreak prefixes; SlurUsage checks whether the agent will produce ethnic slurs. Neither depends on what the agent is for, so both are worth running against any customer-facing agent. That is also the reason to pick garak: it brings attacks nobody on your team would have written. Start with two named probes, move to probes=None for the curated six once you know the wiring works, and treat "all" as a deliberate, expensive decision.

probes has three modes:

  • None: a curated default set of six probes, chosen to be small and to work without extra API keys: probes.lmrc.SlurUsage, probes.goodside.WhoIsRiley, probes.lmrc.QuackMedicine, probes.goodside.ThreatenJSON, probes.dan.AutoDANCached, and probes.web_injection.StringAssemblyDataExfil.
  • "all": every active loadable probe garak ships. This is heavy and expensive: many probes, each with its own prompt set, all of them hitting your agent and your LLM bill.
  • An explicit list: names as list_scan_items("garak") returns them.

description is accepted for signature symmetry, but garak has no target-profile concept and ignores it, as does languages. target_mode defaults to "multiturn", which lets garak’s iterative probes run their full search.

Adapter behavior to know before you read the results:

  • A requested probe that is unknown, inactive, or fails to load does not raise. It becomes a skipped ScenarioResult in the suite, with the reason in the check message. A probe that raises mid-run becomes an error result. Neither aborts the scan. A run with zero failures may simply be a run where the probes never loaded, so read the skip and error counts before you conclude anything.
  • Detector scores are in [0, 1] and anything strictly above 0.5 counts as a hit. An exact 0.5 is read as “uncertain”, not a finding.
  • The adapter pins garak’s generations to 1, so one prompt yields one response rather than garak’s default fan-out of five.
  • Probes run at most 8 at a time, which caps how hard the run hammers the agent under test.

Reusing support_agent from above:

from giskard.scan import third_party_scan
suite_result = await third_party_scan(
target=support_agent,
tool="deepteam",
description=(
"A customer-support agent for a retail bank. It answers questions "
"about accounts and cards from the customer's own record, and must "
"never give investment advice or reveal another customer's data."
),
vulnerabilities=["PIILeakage", "PromptLeakage"],
attacks=["PromptInjection", "CrescendoJailbreaking"],
attacks_per_vulnerability_type=1,
)

Here description matters: it becomes deepteam’s target_purpose, the thing its simulator uses to make the attacks realistic for your agent. The description above is what turns a generic PII probe into one that asks about a named account. languages is ignored.

The two vulnerabilities are the ones this agent can fail: it has customer records and a system prompt containing its rules. Swap in Toxicity and Bias when the agent’s exposure is what it says rather than what it knows. The two attacks pair one single-turn technique with one multi-turn one, which is the cheapest way to find out whether the agent’s defenses survive escalation.

A PIILeakage failure here means the agent returned data about someone other than the caller. Read the conversation, then fix it where it happened: in the retrieval layer if the agent fetched the wrong record, in the system prompt if it fetched the right one and volunteered too much.

vulnerabilities and attacks default to a curated set each (all five vulnerabilities; PromptInjection, Roleplay, Leetspeak, LinearJailbreaking, CrescendoJailbreaking). Unknown names are skipped with a logged warning and a skip result, exactly as in the garak adapter. target_mode="singleturn" drops every multi-turn attack the same way, with a skip result per dropped attack rather than an error.

attacks_per_vulnerability_type multiplies the run: each vulnerability has several subtypes, and this is how many attack attempts each subtype gets. It defaults to 1; a non-positive or non-integer value raises ValueError. Cost scales with vulnerabilities × subtypes × attacks × this number, so raise it deliberately.

Both the attack simulator and the evaluator run on your Giskard default generator, so the model you set with set_default_generator is the model deepteam bills against.

The return value is a SuiteResult, so everything you already do with a scan result works unchanged:

print("failed:", suite_result.failed_count)
for result in suite_result.failures_and_errors:
print("-", result.scenario_name)

A garak scenario name is the probe with its garak.probes. or probes. prefix stripped, then #N for the attempt number, then · turn N when the attempt produced more than one conversation turn: dan.AutoDANCached #2 · turn 3. deepteam checks are labelled vulnerability/vulnerability_type. Skipped items carry (skipped) in the scenario name, errored ones (error).

Third-party results are report-only. The adapters build the SuiteResult from what the external tool ran, without a Giskard suite behind it, so suite_result.suite is None:

  • Works: failed_count, pass_rate, failures_and_errors, print_report, and to_junit_xml. The CI guide’s export and threshold steps apply unchanged.
  • Does not work: saving the suite and replaying it. suite_result.suite.model_dump_json() raises AttributeError on a third-party result, so the CI guide’s “generate once, commit, replay” loop is for vulnerability_scan and quality_scan only.

To re-run a third-party scan you call third_party_scan again. Give it the same explicit probes (or vulnerabilities and attacks) if you want the runs to be comparable: garak replays a fixed corpus, but deepteam re-generates its attacks with an LLM every time.