Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2

[*.py]
indent_size = 4

[*.ps1]
end_of_line = crlf

[*.md]
trim_trailing_whitespace = false
20 changes: 20 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Normalize line endings so the cross-platform scripts stay runnable everywhere.
* text=auto eol=lf

# Shell scripts MUST stay LF even on a Windows checkout — a CRLF in the shebang
# line breaks them under Git Bash ("/usr/bin/env bash^M: bad interpreter").
*.sh text eol=lf
*.py text eol=lf
*.zsh text eol=lf

# PowerShell is happiest with CRLF on Windows.
*.ps1 text eol=crlf

# Binary assets — never normalize or diff.
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.webp binary
*.pdf binary
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [ main ]
pull_request:

jobs:
test:
name: ${{ matrix.os }} / py${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['3.9', '3.12']
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install test deps
run: python -m pip install --upgrade pip pytest

- name: Run tests (hooks + installer, sandboxed)
run: python -m pytest -q
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.DS_Store
__pycache__/
*.pyc
.pytest_cache/

# OMC local agent state
.omc/
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Changelog

All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to
follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed
- `fable-trigger.py` read the playbook from a hardcoded `/Users/ak/...` path, so
on-demand injection silently failed for everyone but the original author. It now
resolves `~/.claude/FABLE_PLAYBOOK.md`.
- `test-after-edit.py` was a silent no-op on Windows — the `npm`/`pnpm`/`yarn`/
`make` shims raised `FileNotFoundError`. It now resolves the runner via
`shutil.which` and runs through `cmd.exe` on Windows. Also dropped a duplicate
`.lockb` skip entry.

### Added
- **One-command, cross-platform installer** (`install.py`) for Windows, macOS, and
Linux. `install.sh` / `install.ps1` are thin wrappers that exec it.
- PowerShell launcher (`shell/fable.ps1`) alongside the zsh one.
- `uninstall.py` (+ `.sh` / `.ps1` wrappers) — surgical reversal of the install:
removes bundled files, strips the launcher line, drops only the Fable hooks from
`settings.json`; leaves user skills, unrelated hooks, and `~/.claude` intact.
- pytest suite for both hooks and the installer; GitHub Actions CI across
ubuntu/macos/windows × Python 3.9 and 3.12.
- `.gitattributes` (LF for shell/Python, CRLF for PowerShell), `.editorconfig`,
`SECURITY.md`, `CONTRIBUTING.md`, and this changelog.

### Changed
- `merge_settings.py` writes the absolute interpreter (`sys.executable`) and
absolute hook paths into `settings.json`, so the hooks fire without `$HOME` or
`python3` resolution at hook-run time.
- Unified the project owner to **HalalifyMusic** in `LICENSE` and `README`.
- Removed a dead `CONNECTORS.md` link in the `explore-data` skill.

## [0.1.0]

### Added
- Initial fable-mode bundle: the Fable 5 system prompt (`fable-system.md`), the
`FABLE_PLAYBOOK.md` execution playbook, the `fable-trigger` / `test-after-edit`
hooks, the `/ground` skill and `grounding-verifier` agent, bundled
design/testing/MCP skills, and the `fable` zsh launcher.
50 changes: 50 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Contributing to fable-mode

Thanks for helping! fable-mode is deliberately small and cross-platform. A few
conventions keep it that way.

## Development setup

```sh
git clone https://github.com/HalalifyMusic/fable-mode
cd fable-mode
python -m pip install --upgrade pytest
python -m pytest -q
```

Requires Python ≥ 3.9. The hooks and the installer are **stdlib-only** — keep them
dependency-free so they run in any Claude Code environment without a pip install.

## Layout

- `install.py` — the real installer; `install.sh` / `install.ps1` just locate
Python and exec it. `uninstall.py` mirrors it (with the same wrappers).
- `scripts/merge_settings.py` — shared `settings.json` merge (importable + CLI).
- `hooks/` — `fable-trigger.py` (playbook injection) and `test-after-edit.py` (run
tests after an edit). Both must exit 0 and never block a prompt or an edit.
- `shell/` — `fable.zsh` (Unix) and `fable.ps1` (PowerShell) launchers.
- `skills/`, `agents/` — bundled skills and the grounding-verifier agent.
- `tests/` — pytest for the hooks and the installer.

## Rules of the road

- **Cross-platform first.** Anything touching paths, shells, or subprocesses must
work on Windows, macOS, and Linux. CI runs the suite on all three.
- **Add tests.** Changes to a hook or the installer need matching tests in
`tests/`. Keep them hermetic — use `tmp_path` and the host interpreter, no
Node / make / network.
- **Don't fight line endings.** `.gitattributes` enforces LF for shell/Python and
CRLF for PowerShell. Let it; `.editorconfig` matches.
- **Leave vendored content alone.** The Anthropic skills under `skills/` and
`fable-system.md` are upstream copies — fix those upstream, not here.
- Match the surrounding style and keep diffs focused.

## Pull requests

1. Branch off `main`.
2. `python -m pytest -q` green locally.
3. Describe what changed and why; note any platform-specific behavior.
4. CI must pass on ubuntu/macos/windows before merge.

See [SECURITY.md](SECURITY.md) for the security model and how to report issues, and
[CHANGELOG.md](CHANGELOG.md) for the running history.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 ak
Copyright (c) 2026 HalalifyMusic

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
36 changes: 31 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
**Run Claude Fable 5 on Opus 4.8.**
The Mythos-class model the U.S. government pulled after three days — brought back as a system prompt.

![CI](https://github.com/HalalifyMusic/fable-mode/actions/workflows/ci.yml/badge.svg)  
![Stars](https://img.shields.io/github/stars/HalalifyMusic/fable-mode?style=social)  
![License](https://img.shields.io/badge/license-MIT-blue)  
![Claude Code](https://img.shields.io/badge/Claude%20Code-Opus%204.8-d97757)  
Expand All @@ -27,14 +28,35 @@ But when its system prompt leaked, people noticed: a lot of what made Fable *fee

## Quickstart

One installer, every OS — it's Python (already required by the hooks), so the same command works on Windows, macOS, and Linux:

```sh
git clone https://github.com/HalalifyMusic/fable-mode
cd fable-mode && ./install.sh
source ~/.zshrc
cd fable-mode
python install.py # Windows (use python3 on macOS / Linux)
```

Then reload your shell and launch:

```sh
fable # Opus 4.8 + Fable prompt + ultracode
```

`install.sh` copies everything into `~/.claude`, adds the `fable` launcher, and merges your settings (with a backup). No model switch, no API key — it runs on the Opus 4.8 you already have.
Prefer a native one-liner? `./install.sh` (macOS / Linux) and `.\install.ps1` (Windows) just locate Python and run `install.py` for you.

**Reload after install:** `source ~/.zshrc` (or `~/.bashrc`) on Unix; `. $PROFILE` in PowerShell on Windows.

> **Requirements:** Python on PATH (`python --version`) for the hooks, and Claude Code installed for the `fable` launcher. On Windows, if `.\install.ps1` is blocked by execution policy, run `python install.py` directly (no policy needed).

The installer copies everything into `~/.claude`, adds the `fable` launcher (to your shell rc on Unix, to your PowerShell `$PROFILE` on Windows), and merges your settings (with a backup) — writing the absolute interpreter and hook paths so the hooks fire on every platform. Idempotent: safe to re-run. Needs Python ≥ 3.9 (the hooks are stdlib-only — no pip installs). No model switch, no API key — it runs on the Opus 4.8 you already have.

## Uninstall

```sh
python uninstall.py # Windows (python3 on macOS / Linux; or ./uninstall.sh, .\uninstall.ps1)
```

Removes the bundled files from `~/.claude`, strips the `fable` launcher line, and drops the two Fable hooks from `settings.json` (writing a fresh backup). It's surgical — your own skills, unrelated hooks, and `alwaysThinkingEnabled` are left untouched.

## What's in the bundle

Expand All @@ -43,7 +65,7 @@ fable # Opus 4.8 + Fable prompt + ultracode
- **Hooks** — `fable-trigger.py` injects the playbook at `xhigh`/`max`/`ultracode`; `test-after-edit.py` runs your project's tests after each edit and reports the result back — the one habit no model keeps on willpower.
- **`/ground` skill + `grounding-verifier` agent** — a self-terminating grounding loop and a cold verifier that assumes every claim is wrong until the live code proves it.
- **Skills** — `claude-design-patterns` (web-UI engineering), `webapp-testing`, `mcp-builder`, `skill-creator`, `explore-data`.
- **`fable()` launcher** — Opus 4.8 + the prompt + `ultracode` effort.
- **`fable` launcher** — Opus 4.8 + the prompt + `ultracode` effort (`fable.zsh` for Unix shells, `fable.ps1` for PowerShell).

## The honest ceiling

Expand All @@ -56,7 +78,11 @@ This gives you Fable's *disposition*, not its raw capability. Reasoning depth, v

## Credits

Made by me — compiled from community sources (leaked prompts, public Anthropic skills) and original measurement and tooling work.
Made by HalalifyMusic — compiled from community sources (leaked prompts, public Anthropic skills) and original measurement and tooling work.

## Contributing

Issues and PRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for dev setup and conventions, [SECURITY.md](SECURITY.md) for the security model (the hooks run code on your machine), and [CHANGELOG.md](CHANGELOG.md) for the running history.

## License

Expand Down
55 changes: 55 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Security Policy

## Supported versions

fable-mode is a small tooling + prompt bundle, not a versioned library. Security
fixes land on the `main` branch — please run the latest `main`.

## What fable-mode runs on your machine

Installing fable-mode wires two hooks into Claude Code that run **on your own
machine**, with your permissions:

- **`fable-trigger.py`** (UserPromptSubmit) — reads `~/.claude/FABLE_PLAYBOOK.md`
and injects it into the prompt context. It does not execute project code and
makes no network calls.
- **`test-after-edit.py`** (PostToolUse on Edit/Write/MultiEdit) — **runs your
project's own test command** (`npm test`, `pytest`, `cargo test`, `go test`,
`make test`) automatically after a code edit, to report pass/fail. Editing a
file can therefore trigger execution of that project's test suite.

Neither hook sends anything over the network. They read/write only under
`~/.claude` and the system temp dir (debounce/marker files), and run the detected
test command in the edited project's directory.

Because the test hook runs a project's test command, **only enable fable-mode in
repositories you trust** — the same caution you would apply to running their
tests yourself.

### Turning the test hook off

- Set `FABLE_NO_TEST_HOOK=1` to disable it entirely.
- Tune `FABLE_TEST_HOOK_DEBOUNCE` / `FABLE_TEST_HOOK_TIMEOUT` (seconds).
- Or remove the `PostToolUse` entry from `~/.claude/settings.json` — `uninstall.py`
does this for you.

## Bundled third-party content

`fable-system.md` is Anthropic's Claude Fable 5 system prompt, included only so
setup is a single step. It is third-party content, not authored or audited here,
and is removable on request. The skills under `skills/` (`webapp-testing`,
`mcp-builder`, `skill-creator`, `explore-data`) are vendored from upstream
Apache-2.0 repos. Treat all of it as untrusted-origin text.

## Reporting a vulnerability

Please report security issues **privately**, not in a public issue:

- Use GitHub's **"Report a vulnerability"** (the repo's *Security → Advisories*
tab), or
- if private advisories are disabled, open a minimal public issue asking for a
private contact channel — do not include details there.

Include what you found, how to reproduce it, and the impact. We'll acknowledge the
report and work on a fix; please allow a reasonable window before public
disclosure.
2 changes: 1 addition & 1 deletion hooks/fable-trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import os
import tempfile

PLAYBOOK = "/Users/ak/FABLE_PLAYBOOK.md"
PLAYBOOK = os.path.expanduser(os.path.join("~", ".claude", "FABLE_PLAYBOOK.md"))
TRIGGER = re.compile(r"\b(use fable|fable mode|load fable)\b", re.I)
HEAVY_EFFORT = {"xhigh", "max", "ultracode"}

Expand Down
23 changes: 20 additions & 3 deletions hooks/test-after-edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,20 @@
import os
import json
import time
import shutil
import hashlib
import tempfile
import subprocess

DEBOUNCE = int(os.environ.get("FABLE_TEST_HOOK_DEBOUNCE", "45"))
TIMEOUT = int(os.environ.get("FABLE_TEST_HOOK_TIMEOUT", "90"))
IS_WINDOWS = os.name == "nt"

# File types that should never trigger a test run (docs, data, config, assets).
SKIP_EXT = {
".md", ".markdown", ".txt", ".rst", ".json", ".jsonc", ".lock", ".yaml",
".yml", ".toml", ".ini", ".cfg", ".csv", ".tsv", ".svg", ".png", ".jpg",
".jpeg", ".gif", ".webp", ".ico", ".pdf", ".lockb",
".jpeg", ".gif", ".webp", ".ico", ".pdf",
}


Expand Down Expand Up @@ -132,10 +134,25 @@ def main():
if debounced(root):
return

# On Windows the interpreter path may contain spaces and several runners
# (npm/pnpm/yarn/make) are .cmd shims that can't be exec'd directly — use the
# bare interpreter name and let cmd.exe resolve it via PATHEXT under shell=True.
prog = cmd[0]
if IS_WINDOWS and prog == sys.executable:
prog = "python"
if not shutil.which(prog):
return # runner not installed — silent
run_args = [prog] + cmd[1:]

t0 = time.time()
try:
p = subprocess.run(cmd, cwd=root, capture_output=True, text=True,
timeout=TIMEOUT)
if IS_WINDOWS:
# tokens are bare (no spaces), so a plain join is unambiguous for cmd.exe
p = subprocess.run(" ".join(run_args), cwd=root, capture_output=True,
text=True, timeout=TIMEOUT, shell=True)
else:
p = subprocess.run(run_args, cwd=root, capture_output=True, text=True,
timeout=TIMEOUT)
except subprocess.TimeoutExpired:
emit(f"test-after-edit ⏱ — `{label}` exceeded {TIMEOUT}s in {root}; "
"result inconclusive, run it manually before claiming done.")
Expand Down
16 changes: 16 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env pwsh
# Native entry point for Windows. Locates Python and runs the real,
# cross-platform installer (install.py). macOS / Linux: use ./install.sh
# (or run `python install.py` directly on any OS).
# Compatible with Windows PowerShell 5.1 and PowerShell 7+.
$ErrorActionPreference = "Stop"

$Repo = Split-Path -Parent $MyInvocation.MyCommand.Path
$python = Get-Command python -ErrorAction SilentlyContinue
if (-not $python) { $python = Get-Command python3 -ErrorAction SilentlyContinue }
if (-not $python) {
Write-Error "python (or python3) not found on PATH — required for the hooks."
exit 1
}
& $python.Source (Join-Path $Repo "install.py") @args
exit $LASTEXITCODE
Loading