Recorded against a local stub endpoint, so no API spend.
scamscript
A reverse-engineering analyzer for leaked or public scam call scripts and vishing transcripts. Point it at a script and a model takes the fraud apart: the persona, the hook that catches you, how trust is manufactured, the pressure applied, what happens when you hesitate, and how to counter that exact playbook.
Security research and fraud-awareness training only. Output is a model-produced reading of a captured script, not legal advice and not attribution. A script is evidence of a pattern; the live scam that reuses it will differ in details.
Why
Scripts leak. Call centres dump their flows, victims publish transcripts, researchers collect
samples. Reading them one by one teaches you that your account is at risk is bad, but not how a
particular operation sequences trust into a transfer. This tool turns each artifact into a
comparable record, so a corpus can be ranked and a defender's briefing can quote the actual
mechanics instead of general advice.
Quickstart
pip install scamscript # or, for development:
# git clone https://github.com/sjdsjadjajdjsa/scamscript && cd scamscript && pip install -e ".[dev]"
export API_KEY=...
export SCAMSCRIPT_BASE_URL=https://openrouter.ai/api/v1
export SCAMSCRIPT_MODEL=deepseek/deepseek-v4.1-flash-1017
scamscript --dry-run analyze --file examples/tech_support_scam.txt # inspect the prompt, spend nothing
scamscript analyze --file examples/tech_support_scam.txt
Any OpenAI-compatible /chat/completions endpoint works. There is no baked-in provider or model:
pass --base-url and --model, or set SCAMSCRIPT_BASE_URL and SCAMSCRIPT_MODEL.
scamscript --base-url https://openrouter.ai/api/v1 \
--model deepseek/deepseek-v4.1-flash-1017 analyze --file script.txt
scamscript --base-url http://localhost:11434/v1 --model llama3.2 analyze --file script.txt
What it produces
Each script renders as a colour-coded panel:
╭──────────────────────────── tech_support_scam.txt ────────────────────────────╮
│ ATTACK TYPE TECH SUPPORT SCAM SOPHISTICATION 6/10 │
│ │
│ OPENING HOOK │
│ "Our telemetry shows your machine has been broadcasting error ..." │
│ │
│ RED FLAGS │
│ ✗ Unsolicited call claiming Windows telemetry │
│ ✗ Caller asks for a Remote Desktop session ID │
╰────────────────────────────────── 7 red flags ───────────────────────────────╯
Fields: attack_type, channel, persona, opening_hook, trust_building_moves, urgency_tactics,
escalation_ladder, target_demographics, red_flags, defense_summary, sophistication_score.
The panel also carries a quote check: every quoted claim in opening_hook and red_flags is
compared against the source, and lines that are paraphrased or simply not there are marked. Use
--strict-quotes to make a missing line fail the run, and --no-verify to skip it.
Batch mode ranks a whole directory into one markdown report. A redacted report from the sample
corpus is at docs/sample-report.md.
Commands
| Command | Purpose |
|---|---|
analyze --file |
Break down one script |
analyze --dir --batch |
Directory, aggregated report |
batch <dir> |
Same as above, ranked |
report --run-id |
Regenerate a report from a past run's log |
history |
List previous runs |
brief |
One-page defence handout from a finished run |
prune |
Delete old logs |
Useful flags: --dry-run, --json, --redact, --top N, --recursive, --timeout,
--retries, --max-tokens, --concurrency, --max-cost, --dedupe/--no-dedupe,
--show-duplicates, --type, --channel, --language, --force, --no-color. --json puts one object per script on stdout and all
Rich output on stderr, so it pipes:
scamscript --json batch examples | jq -s 'map(select(.event == "result")) | length'
Scale and spend
--concurrency N runs up to 16 analyses at once and keeps output in script order. --max-cost
stops scheduling new scripts once the provider reports that much spend, marks the remainder skipped
and still exits zero when nothing failed.
scamscript --concurrency 6 --max-cost 0.25 batch corpus/ --output report.md
The cap reads usage.cost from the response, so it works only where the provider reports cost and
says so when none is sent. Spend is printed and logged per run. In-flight requests can overshoot by
up to the concurrency, so keep headroom on a hard limit.
Real corpus
corpus/real/ holds ten published transcripts with source, date and verbatim status in every
header: a 40-minute bank impersonation call, a Medicare benefits pitch, six FTC robocalls, a
recalled credit-union call and a USPS smishing message. Provenance and the results of running them
are in docs/real-corpus.md.
scamscript --redact batch corpus/real --concurrency 3 --max-cost 0.25 --output real.md
Attack types
attack_type is free prose, which is useful to read and useless to group. Every result also carries a
closed-set slug (vishing, smishing, tech-support, refund, gov-impersonation, bec,
romance, crypto, investment, utility, delivery, other). Reports show counts and mean scores
per slug above the ranking, and --type filters to the ones you ask for:
scamscript batch corpus/ --type tech-support,refund --output report.md
Delivery is tracked separately from theme. channel is one of vishing, smishing, email or
other, so a delivery-themed text lure is delivery + smishing rather than losing one of the two.
--channel filters on it:
scamscript batch corpus/real --channel smishing --output texts.md
Briefings group by type but accept --channel too. Build one from a finished run:
scamscript brief --type tech-support --output briefing.md
The raw phrase is always kept next to the slug. An unrecognised description lands in other and the
CLI names any filter token it did not recognise.
Duplicates
Corpora are full of reprints. Each script is fingerprinted on its body: headers, casing, punctuation
and whitespace are ignored, so copies collapse. The strongest copy is ranked and the rest are listed
under it; the report counts what was folded and JSON records carry fingerprint and duplicate_of.
scamscript batch corpus/ --output report.md # folds by default
scamscript batch corpus/ --no-dedupe --show-duplicates
Only byte-level twins fold. A copy with one changed account number stays separate, by design.
How it works
script ──▶ ingest (type/size guards, optional redaction)
──▶ analyst prompt ──▶ /chat/completions
──▶ JSON extraction ──▶ Pydantic schema
│ invalid / truncated
└──▶ one repair retry with the error fed back
──▶ Rich panel | JSONL record | ranked markdown
- Validation. Replies are parsed tolerantly (fences, prose, string lists,
9/10scores) and then validated. One malformed reply earns exactly one repair attempt; a second failure is reported, not retried forever. - Transport. Retryable statuses (408, 409, 425, 429, 5xx) and empty replies back off and
retry within
--retries;Retry-Afteris honoured. Afinish_reason: lengthreply is treated as truncation and repaired with a shorter-answer instruction. - Reasoning models. If a provider returns only reasoning text with
finish_reason: length, the client treats it as truncation, and when a provider parks the answer inreasoningwith an emptycontent, that text is used as a last resort. Raise--max-tokensfor models that think first. - Quote checking. Quoted claims are normalised (case, quotes, punctuation) and matched against
the script.
closemeans a paraphrase,missingmeans the line is not in the artifact. Verification never drops a finding, it only marks it for the reader. - Scoring.
sophistication_scorefollows fixed anchors (1-2 one device, 5-6 staged trust and one deadline, 9-10 rehearsed objection handling and controlled transfers) so rankings compare across runs instead of drifting with prose.
Redaction and logs
--redact masks emails, card numbers, SSNs, phone numbers and long digit runs before the prompt
is sent, before anything is logged, and inside the returned breakdown. Counts are logged. Regex
masking is best effort: names, case IDs and spelled-out digits survive it.
Every request appends to a timestamped JSONL file under ~/.scamscript/logs/ (override with
--home or SCAMSCRIPT_HOME). The API key is never written. Logs are the source for report, so
a run replays without spending tokens, and prune --keep 200 --keep-days 30 keeps the store
bounded. Directories are 0700 and log files 0600.
Printable briefings
brief --html writes a self-contained page with inline CSS and escaped text, for handing to people
who will not open a terminal.
scamscript brief --type tech-support --html --output handout.html
Paths
--file and --dir resolve symlinks, a recursive walk survives a symlink loop, and a file that
changes while it is read is an error rather than a half-read prompt. Output files are not overwritten
unless --force is passed.
Install and configure
Python 3.12+. pip install scamscript for use, pip install -e ".[dev]" from a clone for tests.
Release history is in CHANGELOG.md.
| Setting | Flag | Env | Default |
|---|---|---|---|
| API key | --api-key |
API_KEY |
none, required unless --dry-run |
| Base URL | --base-url |
SCAMSCRIPT_BASE_URL |
none, required |
| Model | --model |
SCAMSCRIPT_MODEL |
none, required |
| State dir | --home |
SCAMSCRIPT_HOME |
~/.scamscript |
Tests, CI and calibration
pytest
The suite mocks the HTTP layer with httpx.MockTransport; no network or key needed. CI runs the
suite and a wheel build on Python 3.12 and 3.13.
Calibration is evidence, not vibes. scripts/calibrate.py measures first-attempt schema
adherence, repair rate, latency, score spread and billed cost against a live endpoint:
API_KEY=... SCAMSCRIPT_BASE_URL=https://openrouter.ai/api/v1 \
python scripts/calibrate.py --dir examples --repeat 3 \
--model deepseek/deepseek-v4.1-flash-1017
Recorded passes with their numbers are in docs/calibration.md: three rubric
repeats, a three-model comparison, an adversarial set and end-case stability runs. The current rubric
reaches both ends of the scale, and quote checking flags the invented lines a plain reading would
take as evidence.
The adversarial set lives in examples/adversarial/ and has its own harness:
API_KEY=... SCAMSCRIPT_BASE_URL=... python scripts/injection_eval.py --model ...
Format held in 7/7 cases; embedded demands were obeyed in 1 of 6. The fence protects the schema, not the classification.
Layout
src/scamscript/
cli.py command wiring, exit codes
client.py OpenAI-compatible client, retries, truncation
analysis.py per-script loop, one repair retry
ingest.py file and directory reading with guards
models.py Pydantic schema, tolerant JSON extraction
prompts.py analyst prompt and score rubric
redact.py identifier masking
render.py Rich panels and summary
report.py ranked markdown aggregation
store.py JSONL log, run discovery, retention
verify.py quote checking against the source
tests/ schema, retry, redaction, verification, client and CLI tests
examples/ six fictional transcripts, plus adversarial/ fixtures
corpus/real/ ten published transcripts with provenance
scripts/ calibration and injection harnesses
docs/ calibration record, sample report, banner, demo
Limits
- Breakdowns are model readings; a bad prompt or a weak model produces confident nonsense. Read
opening_hookandred_flagsagainst the source before quoting anything. - Redaction is regex-based and will miss names and identifiers without a fixed shape.
- Duplicate folding ignores formatting only; a single changed digit keeps a copy separate.
- Type slugs are keyword-matched, so an unusual description falls to
otherrather than inventing one. - The cost cap depends on provider-reported usage and can overshoot by the in-flight count.
channelis inferred from the model's answer or, failing that, from keywords; a script with no channel words falls toother.- The prompt asks the model to answer in the script's own language and
--languageoverrides detection, but the type keywords are English: a non-English script often falls toother. - Quote checking compares text, not meaning. A legitimate paraphrase left in quotes is flagged; an accurate paraphrase without quotes is skipped rather than judged.
Scope and disclaimer
For security research and fraud-awareness training only. Not legal advice, not an attribution tool, no warranty. Keep API keys out of commits; the key is read from the environment.
MIT licensed, see LICENSE. Contribution notes in CONTRIBUTING.md, vulnerability reports in SECURITY.md.
Metadata
Release files for scamscript 0.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| scamscript-0.8.0.tar.gz | 467.4 kB | With provenance | Details for scamscript-0.8.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| scamscript-0.8.0-py3-none-any.whl | Python 3 | none | any | Details for scamscript-0.8.0-py3-none-any.whl |
Total release size: 510.2 kB
Release files / scamscript-0.8.0.tar.gz
| Download URL | scamscript-0.8.0.tar.gz |
|---|---|
| Size | 467.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d273ad73f84e41d6e0e48bd91c6c755992894a188d256a30e5ca4c18c200521a
|
|
BLAKE2b-256 checksum How to use checksums |
dbfd8f14d632cb5a566754ccd582761901a13c079525c7e9b3b770130f98cb14
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logRelease files / scamscript-0.8.0-py3-none-any.whl
| Download URL | scamscript-0.8.0-py3-none-any.whl |
|---|---|
| Size | 42.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
159dd227ed8a913472539fef879986a246fdc7357f5df665b42a7b4606220d10
|
|
BLAKE2b-256 checksum How to use checksums |
43f0b5e9578525d92b36af1897659c83e3ec7982e81efefe52390c717265b02c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logSecurity for scamscript 0.8.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for scamscript-0.8.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.