Running Locally
Everything you need to get a stylobot gateway running on your machine, point it at any web app, and start enforcing policy. Self-contained: install through troubleshooting in one page.
Install
Pick the path for your platform. All paths produce the same stylobot binary on $PATH.
macOS (Homebrew)
brew install scottgal/stylobot/stylobot
Windows (Chocolatey)
choco install stylobot
Windows (winget)
winget install Mostlylucid.StyloBot
Debian / Ubuntu (apt, Cloudsmith-signed)
curl -1sLf 'https://dl.cloudsmith.io/public/mostlylucid/stylobot/setup.deb.sh' | sudo bash
sudo apt update && sudo apt install stylobot
Docker (any OS, bundles gateway + dashboard)
docker run -p 8080:8080 scottgal/stylobot-all:latest
Then open http://localhost:8080/_stylobot. stylobot-gateway is the proxy-only image; stylobot-sidecar is the 36 MB AOT detector your app calls directly.
Embed in ASP.NET Core (no gateway, in-process)
dotnet add package Mostlylucid.BotDetection
builder.Services.AddBotDetection();
app.UseRouting();
app.UseBotDetection(); // after UseRouting, before MapControllers
Skip the rest of this page if you go this route. See Embedding in an ASP.NET Core app at the bottom for the live verdict accessors.
Verify the binary
stylobot --version
stylobot man # full reference manual built into the binary
Run
Two positional args: listen port, upstream URL.
# Observe-only (verbose logs, never blocks)
stylobot 5080 http://localhost:3000
# Enforce policies on bot traffic
stylobot 5080 http://localhost:3000 --mode production --policy throttle-stealth
Stylobot listens on 5080, proxies to your upstream, runs detection on every request. Live dashboard: http://localhost:5080/_stylobot/.
Verify it works
Three curl calls. Run them from another terminal while the dashboard is open in a browser tab.
# Browser UA
curl -is -A "Mozilla/5.0 (Macintosh) Safari/605.1.15" http://localhost:5080/
# Bare curl
curl -is -A "curl/8.4.0" http://localhost:5080/
# Scraper hitting a sensitive path
curl -is -A "Python/3.13 aiohttp/3.11" http://localhost:5080/.env
| Caller | Expected probability | Expected band | Action |
|---|---|---|---|
| Browser | < 0.10 | VeryLow |
Allow |
| Bare curl | 0.80 | VeryHigh, BotName=curl |
per --policy |
Python on .env |
0.95+ | VeryHigh, BotType=Scraper |
per --policy |
Each response carries:
X-Bot-Detection: true
X-Bot-Probability: 0.82
X-Bot-Confidence: 0.88
X-Bot-RiskBand: VeryHigh
X-Bot-Detectors: UserAgent,Ip,Heuristic
X-Bot-Action: throttle-stealth
X-Bot-ProcessingTime: 0.8ms
Action policies
--policy <name> picks what happens to bot verdicts.
| Policy | Behaviour | Use it when |
|---|---|---|
allow |
Pass through, log only. | Still tuning thresholds. |
throttle-stealth |
Task.Delay then real response. Bots experience a slow site; humans don't notice. |
Public site protection. |
throttle-tools |
429 + Retry-After. |
API gateways. Legitimate tooling backs off. |
challenge |
JS / cookie challenge. | Browser-shaped traffic only. |
block |
403. | High-confidence bots on sensitive paths. |
block-hard |
TCP RST. | Under active DDoS. |
redirect-honeypot |
302 to a slow fake. | Waste their time. |
mask-pii |
Real response with PII redacted. | API where bots try to scrape user data. |
Combine with --threshold 0.7 (default). Probability above this counts as bot.
Per-route overrides live in appsettings.json under BotDetection:Policies and BotDetection:BotTypeActionPolicies:
{
"BotDetection": {
"DefaultActionPolicyName": "throttle-stealth",
"BotTypeActionPolicies": {
"Scraper": "block",
"Tool": "throttle-tools",
"MaliciousBot": "mask-pii"
}
}
}
Common tweaks
TLS
# Bring your own cert
stylobot 443 http://localhost:3000 \
--cert /etc/ssl/example.com.pem --key /etc/ssl/example.com.key \
--mode production
# Or let Stylobot get a Let's Encrypt cert
GATEWAY_HTTPS_DOMAIN=example.com \
stylobot 443 http://localhost:3000 --mode production
Perf profile
balanced is the default. Switch via --profile when traffic shape matters.
| Profile | Picks | Use it for |
|---|---|---|
balanced |
50/50 threads, 10k conns, 30s keep-alive | Default, mixed traffic. |
api |
100/100 threads, 20k conns, 15s keep-alive, 64 KB body cap | JSON APIs, no WebSockets. |
site |
200/200 threads, 10k WebSockets, 120s keep-alive, 1 MB body | Public site with browsers + SignalR. |
highrisk |
50/50 threads, 2k conns, 5s keep-alive, 3s header timeout | Under attack right now. Reject fast. |
STYLOBOT_PROFILE=api stylobot 5080 http://localhost:3000 --mode production
# or
stylobot 5080 http://localhost:3000 --mode production --profile api
Background daemon
stylobot start 5080 http://localhost:3000 --mode production --policy throttle-stealth
stylobot status
stylobot logs
stylobot stop
Or -d as a shorthand for start:
stylobot 5080 http://localhost:3000 --mode production -d
Behind Cloudflare quick-tunnel
stylobot 5080 http://localhost:3000 --tunnel
Bare --tunnel uses a one-shot quick tunnel (random *.trycloudflare.com URL). For a named tunnel, pass the token: --tunnel <token>.
Expose the REST API surface
stylobot 5080 http://localhost:3000 --enable-api
Mounts /api/v1/* (detect, topbots, summary, threats, timeseries, fingerprints, sessions). Requires at least one entry under BotDetection:ApiKeys in appsettings.json. Trusted callers send the key as X-SB-Api-Key: <value> and bypass detection.
{
"BotDetection": {
"ApiKeys": {
"internal-cron": {
"Key": "PASTE_OUTPUT_OF_stylobot_genkey_HERE",
"Name": "Internal cron job",
"Enabled": true
}
}
}
}
Generate the key with stylobot genkey.
Wire an LLM
Default detection runs entirely without an LLM. Adding one unlocks per-fingerprint naming and borderline-verdict escalation.
# Ollama on the same host
ollama pull gemma4:e2b
BotDetection__AiDetection__Provider=ollama \
BotDetection__AiDetection__Ollama__Endpoint=http://localhost:11434 \
BotDetection__AiDetection__Ollama__Model=gemma4:e2b \
BotDetection__EnableLlmDescriptions=true \
stylobot 5080 http://localhost:3000 --mode production
Other providers (openai, anthropic, azure, llamasharp for in-process CPU) take the same Provider=<name> switch plus that provider's subsection: BotDetection__AiDetection__OpenAi__ApiKey=..., etc.
EnableLlmDescriptions=true is the master switch for the background description coordinator (fingerprint names + cluster summaries). Off by default.
Editing configuration
CLI flags cover ~80% of operators. For the rest, dump the effective configuration and edit it:
stylobot --output-config /etc/stylobot/appsettings.json
$EDITOR /etc/stylobot/appsettings.json
stylobot 5080 http://localhost:3000 --config /etc/stylobot/appsettings.json
The dumped file shows every default with every key under BotDetection:*. Highlights:
| Key | Default | Effect |
|---|---|---|
BotDetection:BotThreshold |
0.7 |
Probability above this counts as bot. |
BotDetection:NonAiMinProbability |
0.01 |
Floor on probability when no LLM ran. Stops "0% bot" claims. |
BotDetection:NonAiMaxProbability |
0.90 |
Ceiling on probability when no LLM ran. Stops "100% bot" claims. |
BotDetection:Identity:Enabled |
false |
Set true to write fingerprint_keys rows and render the dashboard radar. |
BotDetection:ThreatIntel:Enabled |
false |
Set true to load Spamhaus DROP, Tor exit list, CISA KEV, cloud-IP ranges. |
BotDetection:ResponseHeaders:Enabled |
true |
Emit X-Bot-Detection-* on every response. |
BotDetection:EnableLlmDescriptions |
false |
Background fingerprint naming via the wired LLM provider. |
BotDetection:SignatureHashKey |
random | 32-byte base64 HMAC key. Set explicitly in production so signatures persist across restarts. |
Three ways to set any key, in precedence order:
- CLI flag (where one exists):
--mode production. - Env var with
__separator:BotDetection__BotThreshold=0.75. appsettings.json(or--config /path/to/your.json).
Embedding in an ASP.NET Core app instead
If you'd rather run detection inside your own .NET process than as a sidecar gateway:
dotnet add package Mostlylucid.BotDetection
builder.Services.AddBotDetection();
app.UseRouting();
app.UseBotDetection(); // after UseRouting, before MapControllers
app.MapGet("/", (HttpContext ctx) =>
{
var isBot = ctx.IsBot();
var confidence = ctx.GetBotConfidence();
var botType = ctx.GetBotType();
return Results.Ok(new { isBot, confidence, botType });
});
For most teams the standalone CLI gateway is simpler. The embed path is for cases where detection state needs to live in the same process as your application logic.
More
- Hit a problem? See Troubleshooting for the symptom-to-fix table.
- Want the runtime model? See How Stylobot Works.
- Full FOSS docs: https://github.com/scottgal/stylobot/tree/main/docs
- Stylobot articles on the Mostlylucid blog: https://www.mostlylucid.net/blog/category/StyloBot