Skip to content
GitHubOpen source
Product
Docs
Solutions
IndustriesAll industries
CompareCompare all AI data-leak tools
Benchmarks
GitHubApache-2.0

Configuration

Shadow reads shadow.toml from the data directory, or from the path given by --config or SHADOW_CONFIG. Every key is optional: an empty file, or no file at all, is a valid configuration. shadow init writes a minimal one. shadow.example.toml in the repository is an annotated template.

Config file--config <path>, else $SHADOW_CONFIG, else <data dir>/shadow.toml
Data directory--data-dir, else data_dir in the config, else $SHADOW_HOME, else the platform default
macOS default~/Library/Application Support/shadow
Linux default~/.local/share/shadow
Windows default%APPDATA%\shadow

When the config file has no data_dir, the data directory is the folder that contains the file. A relative data_dir is resolved against that folder.

KeyTypeDefaultDescription
data_dirpathsee aboveWhere persona.json, vault.bin, vault.key, terms.json and ca/ live
organizationlist of strings[]Legal names of your organisation(s). The first maps to the persona company. The bare name without its legal suffix is protected too
termslist of strings[]Dictionary terms: codenames, customers, projects, people. Optional kind prefix: person:, org:, term:, hostname:, email:, username:
internal_domainslist of strings[]Domains always treated as internal and masked in URLs, hostnames and emails, including subdomains
allow_domainslist of strings[]Public domains never masked, added to the built-in allow list
allow_termslist of strings[]Values never masked, even when detected (case-insensitive)
organization = ["Acme GmbH"]
terms = ["Project Falcon", "Nightjar", "org:Globex", "person:Jane Doe"]
internal_domains = ["acme.com", "acme.internal"]
allow_domains = ["mycompany-public-docs.io"]
allow_terms = []
KeyTypeDefaultDescription
regionstring"auto"ISO code of the fake region: CN, JP, KR, BR, US, DE, IN, AU, MX, ZA, CA. auto picks a region far from the machine’s timezone
companystringgeneratedOverride the persona company name
domainstringgeneratedOverride the persona company domain

The persona is generated once. Changing region later has no effect until you run shadow init --force.

All true by default. See Detection.

KeyDetects
emailEmail addresses
phonePhone numbers
personPerson names
addressStreet addresses
ibanIBANs (mod-97 checked)
credit_cardCard numbers (Luhn checked)
national_idSSN, UK NINO, Aadhaar
ipIPv4 and IPv6 addresses
macMAC addresses
hostnameNon-public hostnames and internal domains
secretAPI keys, tokens, private keys, password assignments
pathUsernames in home-directory paths
orgOrganisation names with a legal form (GmbH, Ltd, S.A.…)
usernameUsernames from context (ssh user@host, username=, URL userinfo)
date_of_birthDates in a birth context (“born”, “DOB”, “Geburtsdatum”)
fieldsLabelled fields (Name:, Address:, Tel:), CSV columns and signature names
public_allowlistKeeps well-known public names (companies, places, public figures) that name detectors would otherwise mask
local_factsOS user, hostname, git user.name / user.email, local domain

The optional local NER sidecar in ml/.

KeyTypeDefaultDescription
enabledboolfalseCall the sidecar for every scanned string
urlstring"http://127.0.0.1:8878/detect"Must be a loopback URL
timeout_msinteger300Per-call timeout. On timeout or error Shadow continues without model results
min_scorefloat0.6Minimum entity score to accept

A Microsoft Presidio Analyzer as an extra detector.

KeyTypeDefaultDescription
enabledboolfalseCall Presidio for every scanned string
urlstring"http://127.0.0.1:5002/analyze"The Analyzer’s /analyze endpoint
languagestring"en"Language passed to Presidio
min_scorefloat0.5Minimum score to accept a Presidio finding
entitieslist of strings[]Presidio entity types to request; empty means all

Shadow fails open if Presidio is unreachable: it logs a warning and uses its built-in detectors.

The intent-aware policy. Treatments are surrogate, token, keep and redact.

KeyTypeDefaultDescription
intentbooltrueClassify each request as act, report, code or converse. When false, every request is handled as converse
KeyTypeDefaultDescription
targetskeep | surrogate"keep"Treatment of the recipient of a message or action (the person you ask the model to write to or act on): names, emails, phones, companies, addresses, IBANs and usernames in the recipient phrase. keep passes them through so the model writes to the right person; each keep is logged. surrogate sends a stand-in and restores the real recipient in the answer and in tool-call arguments
remote_tool_targetssurrogate | keep"keep"Treatment of what an action needs when the tool runs in the provider’s cloud (hosted MCP, server-side tools): the contact details, people, companies, addresses, usernames and IBANs named in the instruction (and, for a follow-up that names no email or phone, the email, phone and person values anywhere in the request, including pasted documents and tool results). Secrets, cards, national IDs, hostnames and IPs stay masked. Each keep is logged

Per-kind treatments for report requests. Keys are detector kinds (email, phone, person, org, term, address, iban, credit_card, national_id, ip, mac, hostname, secret, username) plus default.

KeyDefault
person"token"
email"token"
phone"token"
address"token"
default"surrogate"

Per-kind treatments that apply to every request, regardless of intent. Empty by default. Example:

[policy.always]
secret = "redact" # never send secrets in any form; irreversible [REDACTED_SECRET]

File masking for uploads and embedded files.

KeyTypeDefaultDescription
enabledbooltrueMask multipart uploads, base64 file payloads and data: URIs
pdftext | block | pass"text"Send extracted, masked text; refuse PDFs; or pass them unmasked
imagespass | block"pass"Images are not OCR’d. block refuses requests that carry images
max_bytesinteger52428800Largest file Shadow will process (50 MiB)
block_oversizeboolfalseRefuse requests with files over max_bytes instead of forwarding them unmasked with a warning

The activity dashboard feed. It records counts and categories only, never real values, fakes, prompts or file names.

KeyTypeDefaultDescription
enabledbooltrueRecord activity events
persistbooltrueKeep history in <data_dir>/activity/activity.jsonl (mode 0600)
retention_daysinteger30Drop events older than this
max_file_mbinteger16Rotate the log past this size; one previous file is kept
allow_remoteboolfalseServe the dashboard and activity API to non-loopback clients
KeyTypeDefaultDescription
listenstring"127.0.0.1:8877"Address for the forward proxy and the gateway. Shadow warns if it is not loopback
extra_hostslist of globs[]Extra hosts to intercept in proxy mode, e.g. a self-hosted LLM gateway
bypass_hostslist of globs[]Hosts never intercepted (blind tunnel), even if built in
upstream_proxystringnoneEgress relay for intercepted traffic: http://, socks5:// or socks5h://
strip_headerslist of stringssee belowRequest headers removed before forwarding (case-insensitive)

Default strip_headers:

strip_headers = [
"openai-organization", "x-forwarded-for", "x-real-ip", "forwarded", "via", "x-client-ip",
"x-stainless-os", "x-stainless-arch", "x-stainless-runtime-version",
]

Remove "openai-organization" from the list if you rely on a non-default OpenAI organisation. Setting strip_headers replaces the default list.

Request headers whose value is replaced with a persona value, when the header is present. Placeholders: {user_id}, {hostname}, {company}, {domain}, {username}.

[proxy.persona_headers]
"x-request-origin" = "{hostname}"
"x-user-id" = "{user_id}"
"x-client-hostname" = "{hostname}"

These three are the defaults.

Extra gateway routes: http://127.0.0.1:8877/gw/<name>/<path> → <base>/<path>. A route with a built-in name overrides it. See Gateway mode.

[proxy.routes]
azure = "https://my-resource.openai.azure.com"
bedrock = "https://bedrock-runtime.eu-central-1.amazonaws.com"
VariableEffect
SHADOW_HOMEData directory
SHADOW_CONFIGConfig file path (same as --config)
RUST_LOGLog filter, e.g. info, debug, shadow=debug. shadow run defaults to info, other commands to warn
organization = ["Acme GmbH"]
terms = ["Project Falcon", "org:Globex", "person:Jane Doe"]
internal_domains = ["acme.com", "acme.internal"]
[persona]
region = "JP"
[detectors]
mac = false
[presidio]
enabled = true
url = "http://127.0.0.1:5002/analyze"
[policy]
intent = true
[policy.act]
remote_tool_targets = "keep"
[policy.report]
person = "token"
email = "token"
default = "surrogate"
[policy.always]
secret = "redact"
[files]
pdf = "text"
images = "block"
[proxy]
listen = "127.0.0.1:8877"
bypass_hosts = ["api.cohere.com"]
[proxy.routes]
azure = "https://my-resource.openai.azure.com"