Let Fate Decide
Trail of BitsOfficialPublished by Trail of Bits
- Skill
- Role or persona
- Agent tooling
- CC-BY-SA-4.0
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
let-fate-decide provides a structured, randomized approach to decision-making when users provide ambiguous, nonchalant, or playful prompts. By drawing a 12-house Zodiac Tarot spread, the agent generates a narrative framework to guide its next steps, helping to break analysis paralysis or resolve ties between multiple valid technical approaches. It is designed to be used as a creative lens for discovery and hypothesis generation, particularly in security and engineering contexts, rather than as a replacement for rigorous evidence-based verification.
What it does
- Performs a 12-house Zodiac Tarot spread using cryptographically secure randomness.
- Maps archetypal card meanings to technical contexts like audit stages, failure classes, and domain-specific challenges.
- Provides a structured narrative to help prioritize investigation paths or architectural decisions.
- Supports legacy modes for custom-sized card draws.
- Integrates with technical context lenses to translate abstract readings into actionable engineering tasks.
- Ensures reproducibility and transparency by outputting the full spread in JSON format.
When to use it
- When a user prompt is vague, ambiguous, or lacks specific requirements.
- When the user employs casual or nonchalant language like 'YOLO', 'whatever', or 'let fate decide'.
- When you need to break a tie between multiple reasonable technical approaches.
- When you need a creative, non-obvious angle for security discovery or hypothesis generation.
- When the user's tone is playful and precision is not the primary goal.
Advantages
- Injects genuine entropy into planning to avoid repetitive or biased decision-making.
- Provides a consistent, structured framework for interpreting ambiguous requests.
- Helps surface blind spots by forcing consideration of different archetypal perspectives.
- Maintains a clear separation between the creative 'reading' and the required technical evidence.
- Reduces the need for back-and-forth clarifying questions in casual interactions.
Usage examples
- “I don't know, just pick a way to test this API, let fate decide.”
- The agent draws the 12-house spread, interprets the cards through the 'Audit Pipeline' and 'Domain' lenses, and proposes a specific testing strategy based on the resulting narrative.
- “YOLO, how should I refactor this legacy module?”
- The agent performs a reading to identify potential risks and architectural priorities, then suggests a refactoring plan aligned with the cards' themes.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/trailofbits/let-fate-decide/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip let-fate-decide.zip -d .agents/skills
Documentation by Trail of Bits
Let Fate Decide
When the path forward is unclear, let the cards speak.
Quick Start
Resolve the drawing script from this SKILL.md's installed
location, not the shell's working directory. In the commands below, replace
<resolved-script-path> with that absolute path, keeping the quotes. Relative
links and JSON file paths resolve from the directory containing this SKILL.md.
Run the drawing script:
uv run --no-config "<resolved-script-path>"The script outputs JSON for the default 12 Houses of the Zodiac spread: 12 houses, each with 1 Major Arcana card and 2 Minor Arcana cards. Each house and card includes a
filepath relative to thisSKILL.md's directory.Read each house file and each card's meaning file to understand the draw. For faster reads, use
--contentto include house and card text directly in the JSON:uv run --no-config "<resolved-script-path>" --contentInterpret the spread using the interpretation guide
When the task belongs to a specialized technical workflow, use technical context lenses to translate the reading into an audit, verification, domain, failure-class, or stakeholder lens
Apply the interpretation to the task at hand
When to Use
Vague prompts: The user's request is ambiguous and multiple reasonable approaches exist
Explicit invocations: "I'm feeling lucky", "let fate decide", "dealer's choice", "surprise me", "whatever you think", "YOLO"
Casual delegation: "whatever", "up to you", "your call", "idk", "just do something", "wing it", "I trust you", "doesn't matter", "do what you want", "I don't care", "any approach works", "you pick"
Yu-Gi-Oh energy: "Heart of the cards", "I believe in the heart of the cards", "you've activated my trap card", "it's time to duel"
Shrug-like brevity: Very short prompts that fully delegate the decision without expressing a preference
Redraw requests: "Try again" or "draw again" when no actual system changes occurred (this means draw new cards, not re-run the same approach)
Tie-breaking: When you are about to arbitrarily pick between 2+ valid approaches, draw cards instead of silently choosing one
When NOT to Use
The user has given clear, specific instructions
The task has a single obvious correct approach
As the deciding authority for safety-critical work (security, data integrity, production deployments, release approval, incident response)
The user explicitly asks you NOT to use Tarot
The user's tone is precision-seeking rather than casual -- ask clarifying questions instead to gather actual requirements
Security and Correctness Use
This skill may be used inside a security, audit, or correctness pipeline as a creative lens for discovery: choosing which angle to inspect next, breaking analysis paralysis, generating hypotheses, or surfacing blind spots.
It is never sufficient by itself. In security and correctness contexts, the reading must be followed by ordinary engineering evidence: source review, tests, proofs, traces, reproduction steps, exploitability analysis, or other domain-appropriate verification. Do not treat a favorable card as permission to ship, suppress a finding, skip validation, or overrule a concrete risk.
How It Works
The Draw
The script uses secrets for cryptographic randomness:
Builds separate Major Arcana (22 cards) and Minor Arcana (56 cards) decks
Performs Fisher-Yates shuffles via
secrets.randbelow()(no modulo bias)Deals the default 12 Houses of the Zodiac spread
Each house receives 1 Major Arcana card followed by 2 Minor Arcana cards
Each of the 36 cards independently has a 50% chance of being reversed
The default spread records a conservative unordered-card entropy budget
exceeding 100 bits: roughly log2(C(22,12)) bits from Major Arcana selection,
log2(C(56,24)) bits from Minor Arcana selection (assuming
secrets.randbelow() is cryptographically secure), plus 36 reversal bits. The
exact values are computed and reported in the JSON output under entropy_bits.
The actual ordered assignment of cards to houses contains more entropy.
The Spread
The default spread is 12 Houses of the Zodiac:
| House | Represents | Question It Answers |
|---|---|---|
| 1 | Self | How should this work begin? |
| 2 | Resources | What values, assets, or constraints matter? |
| 3 | Communication | What needs to be clarified or connected? |
| 4 | Foundations | What context or dependency anchors the task? |
| 5 | Creativity | Where should experimentation or delight shape the work? |
| 6 | Practice | What quality, maintenance, or execution concern matters? |
| 7 | Partnership | Who or what must this integrate with? |
| 8 | Transformation | What risk, shared state, or deep change is present? |
| 9 | Exploration | What principle or broader strategy guides the path? |
| 10 | Calling | What delivery or long-term outcome is being served? |
| 11 | Community | What system, network, or shared aspiration is involved? |
| 12 | The Hidden | What blind spot, ending, or unconscious factor matters? |
Within each house, the Major Arcana card sets the archetypal theme and the two Minor Arcana cards provide practical detail.
For compatibility with older workflows, draw_cards.py --legacy returns the
previous 4-card hand, and draw_cards.py --legacy <count> returns a custom
hand of 1-78 cards. A positional count without --legacy is rejected, because
the new default spread has a fixed shape.
Reference Files
Each house's meaning is in its own markdown file under houses/. House files describe how the house applies across technical contexts including building new projects, vulnerability discovery, correctness verification, and common audit, verification, domain, failure-class, and stakeholder workflows.
Each card's meaning is in its own markdown file under cards/:
cards/major/ - 22 Major Arcana (archetypal forces)
cards/wands/ - 14 Wands (creativity, action, will)
cards/cups/ - 14 Cups (emotion, intuition, relationships)
cards/swords/ - 14 Swords (intellect, conflict, truth)
cards/pentacles/ - 14 Pentacles (material, practical, craft)
Interpretation
After drawing, read each house file and each card file, then synthesize meaning. See the interpretation guide for the full interpretation workflow. For cross-domain translation, see technical context lenses.
Key rules:
Reversed cards invert or complicate the upright meaning
Major Arcana cards carry more weight than Minor Arcana
The spread tells a story across all 12 houses; don't interpret cards in isolation
Map abstract meanings to concrete technical decisions
In security, audit, and correctness work, use the reading to choose an investigation path, then require evidence before accepting or dismissing any risk
Never output interpretation as a text-only turn. Include the interpretation alongside your next tool call (the action that implements the chosen option). Prefer
--contentso all 36 card meanings and all 12 house meanings are available from the draw output.
Example Session (House-Level Fragment)
A real reading synthesizes all 12 houses; the fragment below shows only what one house contributes so the format is clear. Do not stop after one house in actual use.
User: "I dunno, just make it work somehow"
[Draw cards]
1st House (Self): The Magician (upright), Five of Swords (reversed),
Ten of Pentacles (upright)
House contribution: The starting stance is resourceful and tool-rich
(Magician), but the practical details warn against combative edge-case work
(Five of Swords reversed) while still favoring maintainable craft
(Ten of Pentacles). This is one input into the overall reading; combine with
the remaining 11 houses before deciding on an approach.The named draw agent returns a more compact form for portent questions:
3 concise bullets covering the dominant theme, the main risk or blind spot,
and the recommended next action.
Error Handling
If the drawing script fails:
Script crashes with traceback: Report the error to the user and skip the reading. Do not invent cards or simulate a draw — the whole point is real entropy.
Card file not found: Note the missing file, interpret the card from its name and suit alone, and continue with the reading.
Never fake entropy: If the script cannot run, do not simulate a draw using your own "randomness." Tell the user the draw failed.
Rationalizations to Reject
| Rationalization | Why Wrong |
|---|---|
| "The cards said to, so I must" | Cards inform direction, they don't override safety or correctness |
| "This reading justifies my pre-existing preference" | Be honest if the reading challenges your instinct |
| "The reversed card means do nothing" | Reversed means a different angle, not inaction |
| "Major Arcana overrides user requirements" | User requirements always take priority over card readings |
| "I'll keep drawing until I get what I want" | One draw per decision point; accept the reading |
| "The reading says the risk is fine" | Cards can suggest what to inspect; only evidence can dismiss a security or correctness concern |
Files
- ATTRIBUTION.mdAdded by AutoGineer939 B
- LICENSELicence file added by AutoGineer19.7 KiB
- SKILL.md10.0 KiB
- agents/openai.yaml236 B
- assets/trail-of-bits-mark.svg3.0 KiB
- cards/cups/ace-of-cups.md627 B
- cards/cups/eight-of-cups.md666 B
- cards/cups/five-of-cups.md603 B
- cards/cups/four-of-cups.md607 B
- cards/cups/king-of-cups.md511 B
- cards/cups/knight-of-cups.md478 B
- cards/cups/nine-of-cups.md634 B
- cards/cups/page-of-cups.md518 B
- cards/cups/queen-of-cups.md502 B
- cards/cups/seven-of-cups.md497 B
- cards/cups/six-of-cups.md496 B
- cards/cups/ten-of-cups.md536 B
- cards/cups/three-of-cups.md482 B
- cards/cups/two-of-cups.md500 B
- cards/major/00-the-fool.md563 B
- cards/major/01-the-magician.md648 B
- cards/major/02-the-high-priestess.md710 B
- cards/major/03-the-empress.md645 B
- cards/major/04-the-emperor.md651 B
- cards/major/05-the-hierophant.md685 B
- cards/major/06-the-lovers.md715 B
- cards/major/07-the-chariot.md704 B
- cards/major/08-strength.md689 B
- cards/major/09-the-hermit.md644 B
- cards/major/10-wheel-of-fortune.md723 B
- cards/major/11-justice.md664 B
- cards/major/12-the-hanged-man.md709 B
- cards/major/13-death.md702 B
- cards/major/14-temperance.md706 B
- cards/major/15-the-devil.md699 B
- cards/major/16-the-tower.md723 B
- cards/major/17-the-star.md653 B
- cards/major/18-the-moon.md759 B
- cards/major/19-the-sun.md722 B
- cards/major/20-judgement.md701 B
- cards/major/21-the-world.md731 B
- cards/pentacles/ace-of-pentacles.md458 B
- cards/pentacles/eight-of-pentacles.md486 B
- cards/pentacles/five-of-pentacles.md427 B
- cards/pentacles/four-of-pentacles.md436 B
- cards/pentacles/king-of-pentacles.md459 B
- cards/pentacles/knight-of-pentacles.md471 B
- cards/pentacles/nine-of-pentacles.md447 B
- cards/pentacles/page-of-pentacles.md465 B
- cards/pentacles/queen-of-pentacles.md452 B
- cards/pentacles/seven-of-pentacles.md461 B
- cards/pentacles/six-of-pentacles.md428 B
- cards/pentacles/ten-of-pentacles.md459 B
- cards/pentacles/three-of-pentacles.md468 B
- cards/pentacles/two-of-pentacles.md448 B
- cards/swords/ace-of-swords.md477 B
- cards/swords/eight-of-swords.md508 B
- cards/swords/five-of-swords.md498 B
- cards/swords/four-of-swords.md453 B
- cards/swords/king-of-swords.md518 B
- cards/swords/knight-of-swords.md615 B
- cards/swords/nine-of-swords.md597 B
- cards/swords/page-of-swords.md504 B
- cards/swords/queen-of-swords.md517 B
- cards/swords/seven-of-swords.md495 B
- cards/swords/six-of-swords.md494 B
- cards/swords/ten-of-swords.md612 B
- cards/swords/three-of-swords.md457 B
- cards/swords/two-of-swords.md497 B
- cards/wands/ace-of-wands.md461 B
- cards/wands/eight-of-wands.md443 B
- cards/wands/five-of-wands.md482 B
- cards/wands/four-of-wands.md461 B
- cards/wands/king-of-wands.md467 B
- cards/wands/knight-of-wands.md547 B
- cards/wands/nine-of-wands.md414 B
- cards/wands/page-of-wands.md449 B
- cards/wands/queen-of-wands.md466 B
- cards/wands/seven-of-wands.md525 B
- cards/wands/six-of-wands.md587 B
- cards/wands/ten-of-wands.md438 B
- cards/wands/three-of-wands.md464 B
- cards/wands/two-of-wands.md445 B
- houses/01-first-house.md1.4 KiB
- houses/02-second-house.md1.5 KiB
- houses/03-third-house.md1.3 KiB
- houses/04-fourth-house.md1.6 KiB
- houses/05-fifth-house.md1.5 KiB
- houses/06-sixth-house.md1.3 KiB
- houses/07-seventh-house.md1.3 KiB
- houses/08-eighth-house.md1.7 KiB
- houses/09-ninth-house.md1.4 KiB
- houses/10-tenth-house.md1.4 KiB
- houses/11-eleventh-house.md1.5 KiB
- houses/12-twelfth-house.md1.6 KiB
- references/INTERPRETATION_GUIDE.md7.2 KiB
- references/TECHNICAL_CONTEXT_LENSES.md4.6 KiB
- scripts/draw_cards.pyScript9.3 KiB
- scripts/pyproject.toml144 B
- scripts/test_draw_cards.pyScript14.1 KiB
scripts/test_draw_cards.py
#!/usr/bin/env python3
"""Tests for draw_cards.py."""
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
import json
import math
import subprocess
import sys
from collections import Counter
from pathlib import Path
# Import the module under test
sys.path.insert(0, str(Path(__file__).parent))
import draw_cards
SKILL_DIR = Path(__file__).parent.parent
def test_build_deck_has_78_cards():
deck = draw_cards.build_deck()
assert len(deck) == 78, f"Expected 78 cards, got {len(deck)}"
def test_build_deck_has_22_major_arcana():
deck = draw_cards.build_deck()
majors = [c for c in deck if c[0] == "major"]
assert len(majors) == 22, f"Expected 22 major arcana, got {len(majors)}"
def test_build_deck_has_56_minor_arcana():
deck = draw_cards.build_deck()
minors = [c for c in deck if c[0] != "major"]
assert len(minors) == 56, f"Expected 56 minor arcana, got {len(minors)}"
def test_build_minor_deck_has_56_cards():
deck = draw_cards.build_minor_deck()
assert len(deck) == 56, f"Expected 56 minor cards, got {len(deck)}"
def test_build_minor_deck_excludes_major_arcana():
deck = draw_cards.build_minor_deck()
assert all(suit != "major" for suit, _ in deck)
def test_build_deck_all_unique():
deck = draw_cards.build_deck()
card_ids = [c[1] for c in deck]
assert len(card_ids) == len(set(card_ids)), "Duplicate card IDs found"
def test_build_deck_four_suits_14_each():
deck = draw_cards.build_deck()
suits = Counter(c[0] for c in deck if c[0] != "major")
for suit in ["wands", "cups", "swords", "pentacles"]:
assert suits[suit] == 14, f"Expected 14 {suit}, got {suits[suit]}"
def test_draw_cards_returns_requested_count():
for n in [1, 2, 4, 10]:
hand = draw_cards.draw_cards(n)
assert len(hand) == n, f"draw({n}) returned {len(hand)} cards"
def test_draw_cards_default_is_4():
hand = draw_cards.draw_cards()
assert len(hand) == 4
def test_draw_zodiac_spread_default_shape():
hand = draw_cards.draw_zodiac_spread()
assert hand["spread"] == draw_cards.SPREAD_NAME
assert len(hand["houses"]) == 12
def test_draw_cards_clamps_to_78():
hand = draw_cards.draw_cards(100)
assert len(hand) == 78
def test_draw_cards_zero_returns_empty():
hand = draw_cards.draw_cards(0)
assert len(hand) == 0
def test_draw_cards_card_structure():
hand = draw_cards.draw_cards(1)
card = hand[0]
assert "suit" in card
assert "card_id" in card
assert "reversed" in card
assert "position" in card
assert "file" in card
assert isinstance(card["reversed"], bool)
assert card["position"] == 1
assert card["file"].startswith("cards/")
assert card["file"].endswith(".md")
def test_draw_cards_positions_are_sequential():
hand = draw_cards.draw_cards(4)
positions = [c["position"] for c in hand]
assert positions == [1, 2, 3, 4]
def test_draw_cards_no_duplicate_cards():
hand = draw_cards.draw_cards(78)
card_ids = [c["card_id"] for c in hand]
assert len(card_ids) == len(set(card_ids)), "Duplicate cards drawn"
def test_zodiac_spread_shape():
spread = draw_cards.draw_zodiac_spread()
assert spread["spread"] == "12 Houses of the Zodiac"
bits = spread["entropy_bits"]
expected_major = round(math.log2(math.comb(22, 12)), 2)
expected_minor = round(math.log2(math.comb(56, 24)), 2)
assert bits["major_arcana"] == expected_major
assert bits["minor_arcana"] == expected_minor
assert bits["reversals"] == 36.0
assert bits["total"] == round(expected_major + expected_minor + 36.0, 2)
assert bits["total"] >= 100, "Entropy budget must clear the 100-bit floor"
assert len(spread["houses"]) == 12
for house in spread["houses"]:
assert set(house) == {"house", "name", "focus", "file", "cards"}
assert house["file"].startswith("houses/")
assert house["file"].endswith(".md")
assert len(house["cards"]) == 3
def test_zodiac_house_files_exist():
spread = draw_cards.draw_zodiac_spread()
for house in spread["houses"]:
path = SKILL_DIR / house["file"]
assert path.exists(), f"Missing house file: {path}"
def test_zodiac_focus_matches_house_domain():
spread = draw_cards.draw_zodiac_spread()
for house in spread["houses"]:
text = (SKILL_DIR / house["file"]).read_text()
domain = next(
line[len("**Domain**: ") :].strip()
for line in text.splitlines()
if line.startswith("**Domain**: ")
)
assert house["focus"] == domain, (
f"focus/Domain drift in {house['file']}: {house['focus']!r} != {domain!r}"
)
def test_zodiac_house_files_cover_technical_contexts():
spread = draw_cards.draw_zodiac_spread()
for house in spread["houses"]:
text = (SKILL_DIR / house["file"]).read_text()
assert "## Building New Projects" in text
assert "## Vulnerability Discovery" in text
assert "## Correctness Verification" in text
assert "## Technical Workflow Lenses" in text
def test_technical_context_lenses_reference_exists():
reference = SKILL_DIR / "references" / "TECHNICAL_CONTEXT_LENSES.md"
text = reference.read_text()
assert "## Audit Pipeline Stage" in text
assert "## Evidence Mode" in text
assert "## Domain Lens" in text
assert "## Failure Class Lens" in text
assert "## Human and Organizational Lens" in text
assert "## House Affinity Map" in text
def test_security_framing_requires_evidence():
skill_text = (SKILL_DIR / "SKILL.md").read_text()
guide_text = (SKILL_DIR / "references" / "INTERPRETATION_GUIDE.md").read_text()
assert "Security and Correctness Use" in skill_text
assert "never sufficient by itself" in skill_text
assert "only evidence can dismiss a security or correctness concern" in skill_text
assert "cannot prove safety, dismiss a finding, or replace" in guide_text
def test_numbered_ranks_are_numeric_across_all_suits():
word_ranks = ("Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten")
for card_file in (SKILL_DIR / "cards").glob("*/*.md"):
if card_file.parent.name == "major":
continue
text = card_file.read_text()
for word in word_ranks:
assert f"**Rank**: {word}" not in text, (
f"{card_file} still uses word-form rank '{word}'; use the digit form"
)
def test_reviewed_cards_avoid_unsafe_shortcuts():
"""Regression guard: these exact phrases were removed in 1.2.0; do not reintroduce.
This is an exact-string blacklist, not a semantic check. Add new bad phrases
here as they are identified during review; rewordings of existing phrases
will not trip this test and must be caught manually.
"""
risky_phrases = (
"Follow your intuition on this one",
"The solution that feels right probably is",
"Ship it.",
"The approach is sound.",
"The approach will succeed.",
"Speed matters here—refine later.",
)
for card_file in (SKILL_DIR / "cards").glob("*/*.md"):
text = card_file.read_text()
for phrase in risky_phrases:
assert phrase not in text, f"{phrase!r} still appears in {card_file}"
def test_zodiac_spread_content_includes_house_content():
spread = draw_cards.draw_zodiac_spread(include_content=True)
for house in spread["houses"]:
assert "content" in house
assert house["content"].startswith("# ")
def test_zodiac_spread_uses_one_major_per_house():
spread = draw_cards.draw_zodiac_spread()
major_cards = []
for house in spread["houses"]:
card = house["cards"][0]
assert card["role"] == "major_arcana"
assert card["suit"] == "major"
major_cards.append(card["card_id"])
assert len(major_cards) == 12
assert len(set(major_cards)) == 12
def test_zodiac_spread_uses_two_minors_per_house():
spread = draw_cards.draw_zodiac_spread()
minor_cards = []
for house in spread["houses"]:
for card in house["cards"][1:]:
assert card["role"] in {"minor_arcana_1", "minor_arcana_2"}
assert card["suit"] != "major"
minor_cards.append(card["card_id"])
assert len(minor_cards) == 24
assert len(set(minor_cards)) == 24
def test_zodiac_spread_all_cards_can_be_reversed():
spread = draw_cards.draw_zodiac_spread()
cards = [card for house in spread["houses"] for card in house["cards"]]
assert len(cards) == 36
assert all(isinstance(card["reversed"], bool) for card in cards)
def test_zodiac_spread_positions_are_sequential():
spread = draw_cards.draw_zodiac_spread()
positions = [card["position"] for house in spread["houses"] for card in house["cards"]]
assert positions == list(range(1, 37))
def test_fisher_yates_preserves_elements():
deck = draw_cards.build_deck()
original = sorted(deck)
draw_cards.fisher_yates_shuffle(deck)
assert sorted(deck) == original, "Shuffle changed deck elements"
def test_fisher_yates_single_element():
deck = [("major", "00-the-fool")]
draw_cards.fisher_yates_shuffle(deck)
assert deck == [("major", "00-the-fool")]
def test_fisher_yates_empty():
deck = []
draw_cards.fisher_yates_shuffle(deck)
assert deck == []
def test_is_reversed_returns_bool():
for _ in range(20):
assert isinstance(draw_cards.is_reversed(), bool)
def test_shuffle_produces_varying_orders():
"""Run 5 shuffles; at least 2 should differ (p(all same) ~ 0)."""
orders = []
for _ in range(5):
deck = draw_cards.build_deck()
draw_cards.fisher_yates_shuffle(deck)
orders.append(tuple(c[1] for c in deck))
assert len(set(orders)) >= 2, "All 5 shuffles identical"
def test_reversal_produces_both_values():
"""Over 100 flips, both True and False should appear."""
results = {draw_cards.is_reversed() for _ in range(100)}
assert True in results, "Never got reversed=True in 100 flips"
assert False in results, "Never got reversed=False in 100 flips"
def test_no_os_urandom_import():
"""Verify os.urandom is not used; os may be imported for path operations."""
source = Path(__file__).parent / "draw_cards.py"
text = source.read_text()
assert "os.urandom" not in text, "os.urandom still referenced"
def test_uses_secrets_module():
"""Verify secrets module is used."""
source = Path(__file__).parent / "draw_cards.py"
text = source.read_text()
assert "import secrets" in text
assert "secrets.randbelow" in text
assert "secrets.randbits" in text
def test_cli_default_output():
"""Run the script and verify JSON output with the default spread."""
result = subprocess.run(
[sys.executable, str(Path(__file__).parent / "draw_cards.py")],
capture_output=True,
text=True,
)
assert result.returncode == 0
spread = json.loads(result.stdout)
assert spread["spread"] == draw_cards.SPREAD_NAME
assert len(spread["houses"]) == 12
def test_cli_legacy_custom_count():
result = subprocess.run(
[sys.executable, str(Path(__file__).parent / "draw_cards.py"), "--legacy", "2"],
capture_output=True,
text=True,
)
assert result.returncode == 0
cards = json.loads(result.stdout)
assert len(cards) == 2
def test_cli_legacy_default_count():
result = subprocess.run(
[sys.executable, str(Path(__file__).parent / "draw_cards.py"), "--legacy"],
capture_output=True,
text=True,
)
assert result.returncode == 0
cards = json.loads(result.stdout)
assert len(cards) == 4
def test_cli_positional_count_without_legacy_is_error():
"""Positional count requires --legacy; the new default has a fixed shape."""
result = subprocess.run(
[sys.executable, str(Path(__file__).parent / "draw_cards.py"), "4"],
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "--legacy" in result.stderr
def test_cli_invalid_arg():
result = subprocess.run(
[sys.executable, str(Path(__file__).parent / "draw_cards.py"), "--legacy", "abc"],
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "Error" in result.stderr
def test_cli_out_of_range():
result = subprocess.run(
[sys.executable, str(Path(__file__).parent / "draw_cards.py"), "--legacy", "0"],
capture_output=True,
text=True,
)
assert result.returncode == 1
result = subprocess.run(
[sys.executable, str(Path(__file__).parent / "draw_cards.py"), "--legacy", "79"],
capture_output=True,
text=True,
)
assert result.returncode == 1
def test_no_secure_randbelow_function():
"""Regression: the custom secure_randbelow must be removed."""
source = Path(__file__).parent / "draw_cards.py"
text = source.read_text()
assert "def secure_randbelow" not in text, "Custom secure_randbelow still exists"
def test_constants_are_immutable():
"""Constants must be tuples to prevent mutation."""
assert isinstance(draw_cards.MAJOR_ARCANA, tuple), "MAJOR_ARCANA is not a tuple"
assert isinstance(draw_cards.RANKS, tuple), "RANKS is not a tuple"
assert isinstance(draw_cards.SUITS, tuple), "SUITS is not a tuple"
assert isinstance(draw_cards.ZODIAC_HOUSES, tuple), "ZODIAC_HOUSES is not a tuple"
def test_draw_cards_rejects_non_int():
"""draw_cards() must reject non-int types cleanly."""
for bad in [None, "3", 2.5, True, False, [4]]:
try:
draw_cards.draw_cards(bad)
assert False, f"draw_cards({bad!r}) should have raised TypeError"
except TypeError:
pass
if __name__ == "__main__":
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
passed = failed = 0
for test in tests:
try:
test()
passed += 1
print(f" PASS {test.__name__}")
except AssertionError as e:
failed += 1
print(f" FAIL {test.__name__}: {e}")
except Exception as e:
failed += 1
print(f" ERROR {test.__name__}: {e}")
print(f"\n{passed} passed, {failed} failed, {passed + failed} total")
sys.exit(1 if failed else 0)
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Needs Python
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Trail of Bits
- Publisher
- Trail of Bits (Official)
- Source
- trailofbits/skills · plugins/let-fate-decide/skills/let-fate-decide · commit e4dd0bb
- Licence
- Creative Commons Attribution-ShareAlike 4.0 International (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Trail of Bits.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download let-fate-decide.zip