Last updated: 2026-06-01 01:05 UTC

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:

  1. CLI flag (where one exists): --mode production.
  2. Env var with __ separator: BotDetection__BotThreshold=0.75.
  3. 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