This document captures the agreed design decisions for Foxglove v1. It serves as the single source of truth for implementation.
- honeyfile — a decoy document produced by
foxglove generate. - honeytoken — a callback URL/hostname minted on a Thinkst Canary console via
foxglove thinkst create, optionally embedded into honeyfile content.
CLI dispatch (generate | thinkst)
├── generate: validation → context ingestion → campaign planning (batched, Pydantic-enforced JSON)
│ → content generation per honeyfile (sequential, optionally embedding honeytokens)
│ → file assembly to disk
└── thinkst: validation → ThinkstClient call → result rendered to stdout
The CLI is organised around two subcommands: generate (produces honeyfiles) and thinkst (manages honeytokens on a Thinkst Canary console).
uv run python -m foxglove generate \
--base-dir /tmp \
--operation op_alpha \
--num-files 99 \
--formats pdf,epdf,docx,xlsx,pptx,md,conf,json,xml \
--teams it,infra,financial \
--company "mid-sized fintech in Luxembourg..." \
--file-content "files present information in IT infra..." \
[--model claude-haiku-4-5] \
[--passwords foobar,Checkbox1] \
[--honeytokens https://x.canary.tools/x.gif,host.y.canarytokens.com] \
[--resume] \
[--dry-run]
| Parameter | Required | Validation | Default |
|---|---|---|---|
--base-dir |
Yes | Must be an existing, writable directory | — |
--operation |
Yes | Regex: ^[a-zA-Z0-9][a-zA-Z0-9._-]{0,254}$ (filesystem-safe) |
— |
--num-files |
Yes | Integer, 1–MAX_FILES (200) |
— |
--formats |
Yes | Comma-separated; each must be in supported set | — |
--teams |
Yes | Comma-separated; each team name max 64 chars | — |
--company |
Yes | Non-empty string, max 256 chars | — |
--file-content |
Yes | Non-empty string, max 1024 chars | — |
--model |
No | Valid Anthropic model identifier | claude-haiku-4-5 |
--passwords |
No | Comma-separated list of passwords for EPDF files | ["abc123", "root"] |
--honeytokens |
No | Comma-separated callbacks (≤20 entries, each ≤256 chars, no whitespace, no empty entries) | — |
--resume |
No | Flag; enables automatic resume without prompt | false |
--dry-run |
No | Flag; outputs plan to screen, writes nothing to disk | false |
Manages honeytokens on a Thinkst Canary console. Tokens minted by Foxglove carry the memo prefix #foxglove, which thinkst list filters on client-side.
foxglove thinkst create --domain-hash <h> --kind <k> --flock <flock_id> --memo "<text>"
foxglove thinkst list --domain-hash <h> [--flock <flock_id>] [--kind <k>] [--json]
foxglove thinkst remove --domain-hash <h> <canarytoken-id>
--domain-hash is your Console's DOMAIN — the unique hash prefixing the URL https://<domain-hash>.canary.tools/api/v1/.... Must match ^[a-z0-9-]+$.
Auth: env var THINKST_API_KEY sent as X-Canary-Auth-Token HTTP header.
Supported --kind values (v1, no-extra-params kinds only): dns, http, aws-id, slack-api, qr-code, windows-dir, wireguard, credit-card, mysql-dump, svn, sql. Kinds requiring file uploads or extra params (doc-msword, cloned-web, signed-exe, …) are rejected with a helpful message.
thinkst create prints the new id plus the callback URL/hostname so the operator can copy them into generate --honeytokens.
thinkst list cursor-loops /api/v1/canarytokens/paginate, then filters memos containing #foxglove client-side. Default output is <canarytoken-id> | <kind> | <flock_id> | <memo>; --json switches to a JSON array (one object per token, fields including created_printable for scripting).
thinkst remove deletes a single id via /api/v1/canarytoken/delete. If Canary blocks deletion because of unacknowledged incidents, the runner prompts y/n and on yes retries with clear_incidents=true.
pdf, epdf, docx, xlsx, pptx, md, conf, json, xml
- All parameters are validated before any API call is made.
- If any parameter is invalid, the program exits immediately with an error message and a non-zero exit code.
- If
--formatscontains an unsupported format, the entire run fails (not a partial skip). - For
generate,ANTHROPIC_API_KEYenvironment variable must be set; fail immediately at startup if missing. - For
thinkst,THINKST_API_KEYenvironment variable must be set; fail immediately if missing.
| Name | Value | Description |
|---|---|---|
MAX_FILES |
200 | Upper limit for --num-files |
BATCH_SIZE |
50 | Files per planning batch |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | CLI validation error (bad parameters) |
| 2 | Environment error (missing API key, etc.) |
| 3 | API error (network, auth, rate limit) |
| 4 | Partial completion (some honeyfiles skipped) |
| 5 | Planning error (LLM returned invalid plan) |
| 6 | Thinkst API error (auth, 4xx, 5xx, incidents block) |
- On startup, Foxglove checks if
<base-dir>/<operation>/MANIFEST.jsonexists. - If manifest exists and
--resumeis passed: resume automatically — load manifest, skip files that already exist on disk, generate the rest. - If manifest exists and
--resumeis NOT passed: prompt the user interactively (y/n). If "yes", resume. If "no", delete all files under the operation root (including manifest) and start fresh. - If no manifest exists and
--resumeis passed: start fresh with a message:"New operation <name>; starting generation". - If no manifest exists and
--resumeis NOT passed: start fresh (normal behavior).
On resume, Foxglove loads the manifest and iterates through its entries. If the file exists on disk, skip it. If not, generate it.
--dry-runoutputs the campaign plan to stdout. No files are written to disk.--dry-runcombined with--resumeon an existing operation: shows only the remaining ungenerated files.--dry-runon a new operation: runs planning and displays the full plan.
Written to <base-dir>/<operation>/MANIFEST.json. Always generated on every generate run.
{
"operation": "op_alpha",
"parameters": {
"base_dir": "/tmp",
"num_files": 99,
"formats": ["pdf", "epdf", "docx", "xlsx", "conf"],
"teams": ["it", "infra", "financial"],
"company": "mid-sized fintech in Luxembourg...",
"file_content": "files present information in IT infra...",
"model": "claude-haiku-4-5",
"passwords": ["abc123", "root"],
"honeytokens": ["https://abc.canary.tools/x.gif"]
},
"files": [
{
"path": "financial/reports/Q3_2025_Revenue.xlsx",
"format": "xlsx",
"summary": "Quarterly revenue breakdown by product line for Q3 2025"
},
{
"path": "it/vpn/vpn_gateway.conf",
"format": "conf",
"summary": "VPN gateway configuration for the Luxembourg office"
},
{
"path": "infra/policies/access_control.pdf",
"format": "epdf",
"summary": "Physical and logical access control policy. Password: abc123"
}
]
}pathis relative to the operation root (<base-dir>/<operation>/).- For
epdfhoneyfiles, the password is included in thesummaryfield as:"<summary>. Password: <password>". - The
parametersblock stores the original CLI parameters for reproducibility and resume validation. parameters.honeytokensis the list passed via--honeytokens(empty list if the flag was omitted). The campaign's honeytoken set is recorded for audit; distribution across individual honeyfiles is LLM-decided and not tracked per-file.
If the LLM generates duplicate file paths in the plan, Foxglove applies macOS-style deduplication:
- First occurrence:
Report.md - Second occurrence:
Report (2).md - Third occurrence:
Report (3).md
Deduplication is applied during plan post-processing, before any files are written.
- Uses Anthropic's structured output via tool use with Pydantic model schemas.
- For operations with more than
BATCH_SIZE(50) files, planning is split into batches of 50 and merged. - The LLM prompt is formatted with operator context:
Consider files created by
{teams}for the company{company}under this context:{file_content}.
- Sequential (one honeyfile at a time, no concurrency).
- Progress output to stdout:
Generating file 68/99: /tmp/op_alpha/foo/bar/foobar.docx - On generation or write failure, log the error and continue to the next file:
Generating file 83/99: /tmp/op_alpha/foo/bar/foobar.pptx Error generating file: <error details> - If any honeyfiles were skipped, exit with code 4 (partial completion).
- When
parameters.honeytokensis non-empty, the per-honeyfile prompt is appended with an instruction asking the LLM to weave each honeytoken naturally into the content (links, references, signatures, footers). Distribution across honeyfiles is LLM-decided; a one-page memo may use none, a directory listing may use several.
- A default password list is provided:
["abc123", "root"]. - The user can override this list via
--passwords foobar,Checkbox1. - During planning, passwords are assigned to EPDF files by cycling through the password list.
- Passwords are recorded in the manifest's summary field for each EPDF entry.
- Encrypted PDFs use the standard
.pdfextension on disk (not.epdf). Theformatfield in the manifest distinguishes them. Any.epdfextensions from the LLM are normalized to.pdfduring post-processing.
httpx.Client(base_url=f"https://{console}.canary.tools", timeout=30).- Auth header
X-Canary-Auth-Token: <THINKST_API_KEY>. truststore.inject_into_ssl()already covers HTTPS verification with the OS cert store.
Foxglove requires an Admin key. Creating and deleting canarytokens are "full API operations" per Thinkst's role matrix — Analyst can only acknowledge/delete alerts, not canarytokens themselves, and Read-Only is view-only. Flock-scoped Admin is preferred over console-scoped Admin when foxglove targets a single flock (smaller blast radius). Refs: How does the API work?, Flock API Keys.
| Action | Endpoint | Method | Body / query |
|---|---|---|---|
| create | /api/v1/canarytoken/create |
POST | kind, memo (already prefixed #foxglove ), flock_id |
| list | /api/v1/canarytokens/paginate |
GET | limit=100, optional flock_id, cursor; client-side #foxglove and kind filter |
| remove | /api/v1/canarytoken/delete |
POST | canarytoken=<id>; on incidents-block, retry with clear_incidents=true after user confirmation |
Foxglove-created honeytokens have memo #foxglove <user-supplied-memo> (plain prefix). The constant FOXGLOVE_TAG = "#foxglove" lives in foxglove.thinkst.__init__. Listing filters the marker client-side because the Canary search endpoint's behaviour with # punctuation is unspecified.
Limited to no-extra-params kinds whose callback is a URL or hostname: dns, http, aws-id, slack-api, qr-code, windows-dir, wireguard, credit-card, mysql-dump, svn, sql.
| Format | Notes |
|---|---|
| Text content with basic formatting | |
| EPDF | Same as PDF, encrypted with a password from the password list |
| DOCX | Formatted content: headings, paragraphs, bullet points |
| XLSX | Tabular data (rows and columns, not a single text block) |
| PPTX | Multiple slides with titles, bullet points, and text |
| MD | Markdown-formatted text |
| CONF | Generic configuration file format (not tied to specific software) |
| JSON | Structured JSON data |
| XML | Generic XML data (not tied to specific schema) |
foxglove/
├── src/foxglove/
│ ├── __init__.py # Package exports
│ ├── __main__.py # Entrypoint (python -m foxglove); dispatches generate vs thinkst
│ ├── cli.py # Root CLI parser (subcommand dispatch) + generate validation
│ ├── planner.py # Campaign planning (LLM structured output)
│ ├── generator.py # Honeyfile content generation orchestration
│ ├── manifest.py # Manifest read/write
│ ├── models.py # Pydantic models (CampaignPlan, FileEntry, OperationParameters, …)
│ ├── resume.py # Resume action logic
│ ├── thinkst/
│ │ ├── __init__.py # FOXGLOVE_TAG, SUPPORTED_KINDS
│ │ ├── cli.py # parse_thinkst_args; ThinkstCreateArgs/ListArgs/RemoveArgs
│ │ ├── client.py # ThinkstClient (httpx); ThinkstApiError, IncidentsBlockedError
│ │ ├── models.py # Pydantic: CreateTokenResult, TokenSummary
│ │ └── runner.py # run_thinkst (create / list / remove)
│ └── writers/
│ ├── __init__.py # Writer dispatch (get_writer)
│ ├── pdf.py # PDF writer
│ ├── epdf.py # Encrypted PDF writer
│ ├── docx.py # DOCX writer
│ ├── xlsx.py # XLSX writer
│ ├── pptx.py # PPTX writer
│ ├── md.py # Markdown writer
│ ├── conf.py # CONF writer
│ ├── json.py # JSON writer
│ └── xml.py # XML writer
├── tests/
│ └── ... # Tests organised by behaviour
├── CLAUDE.md
├── DESIGN.md
├── README.md
└── pyproject.toml