prepare-book — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited prepare-book (Agent Skill) and scored it 96/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Interactively collect book configuration from the user, extract chapters from an EPUB file, validate round-trip fidelity, and create a complete translation workspace.
Print the version on every run:
cabt:prepare-book v1.2.3Then check if $ARGUMENTS contains --version. If it does, stop here -- do not proceed to Step 1 or any further steps.
Collect all configuration from the user before starting extraction.
$ARGUMENTS, use it. Otherwise ask the user: "What is the path to your EPUB file?"After obtaining the path, verify the file exists:
test -f "<epub_path>" && echo "File found" || echo "File not found"If the file does not exist, tell the user the path was not found and ask them to check it.
"Would you like to reuse glossary and translation style from a previous book's workspace? (yes/no)"
a. Ask: "What is the path to the previous book's workspace folder?"
b. Validate the previous workspace:
test -f "<previous_workspace_path>/metadata.json" && echo "Valid workspace" || echo "Not a valid workspace"c. If not valid: tell the user "That path does not contain a valid CABT workspace (no metadata.json found). Please check the path." Then ask for the path again.
d. If valid: read the previous workspace's metadata.json:
cat "<previous_workspace_path>/metadata.json"e. Extract source_language, target_language, genre, and title from the previous metadata.
f. Display to user:
Loaded configuration from previous workspace:
Previous book: <title>
Genre: <genre>
Source language: <source_language>
Target language: <target_language>g. Use these values for workspace creation. Do NOT ask the genre, source language, or target language questions -- they are inherited from the previous workspace.
h. Set reuse_from to the previous workspace path (used in Step 2.5).
a. Genre -- Present the following options and ask the user to choose one:
b. Source language -- Ask the user: "What is the source language of this book?" (e.g., English, Japanese, Korean)
c. Target language -- Ask the user: "What language should the book be translated to?" (e.g., French, Spanish, German)
d. Set reuse_from to null (Step 2.5 will be skipped).
Run the workspace preparation script with all collected information (genre, source language, and target language come from either reuse or fresh input):
python ${CLAUDE_PLUGIN_ROOT}/scripts/prepare_workspace.py "<epub_path>" --genre "<genre>" --source-language "<source_lang>" --target-language "<target_lang>"If the script exits with an error, show the error message to the user and stop. Do not attempt to retry or work around the error.
Parse the JSON output from stdout on success. The output contains workspace_path, title, and chapter_count.
Only run this step if `reuse_from` is set (user chose to reuse from a previous workspace in Step 1). If reuse_from is null, skip to Step 3.
Run the reuse workspace script:
python ${CLAUDE_PLUGIN_ROOT}/scripts/reuse_workspace.py --source-workspace "<reuse_from>" --target-workspace "<workspace_path>"Where <workspace_path> is the workspace_path from Step 2's JSON output.
If the script exits with an error, show the error message to the user and stop. Do not attempt to retry or work around the error.
Parse the JSON output from stdout on success. Report to the user:
Imported from previous workspace:
Glossary: <"Imported (chapter references cleaned)" if had_glossary else "Not found in previous workspace">
Translation rules: <"Imported" if had_rules else "Not found in previous workspace">
Translation style brief: <"Imported" if had_brief else "Not found in previous workspace">After workspace creation (and optional reuse import), classify which extracted entries are translatable content and which should be skipped. This step always runs regardless of whether reuse was used.
Sub-step 1: Extract snippets
Run the snippet extractor to get chapter data for classification:
python ${CLAUDE_PLUGIN_ROOT}/scripts/extract_snippets.py "<workspace_path>"Parse the JSON output. Each entry has: sequence, output_filename, detected_title, snippet (first 500 chars of text content), snippet_end (last 200 chars of text content), and detection_notes.
Sub-step 2: Display detection path
Read translation_mode from metadata.json in the workspace. Display the detection path to the user:
chapters_from_toc: Detected N chapters from Table of Contents.
Running table of contents classification via LLM...files_from_spine: Bad table of contents (absent or only 1 entry) -- switching to LLM chapter detection.
Extracted N files from EPUB spine. Classifying via LLM...Where N is the number of entries from the snippets output.
Sub-step 3: Classify entries
You (the LLM executing this skill) are the classifier. Look at ALL entries together as a batch -- cross-entry context matters. For each entry, consider the detected_title, snippet (beginning of file), and snippet_end (end of file), then decide:
IMPORTANT — mixed content files: A single HTML file may contain non-content (e.g., promotional blurb, legal notice) mixed with actual story/chapter text. If either snippet or snippet_end contains what looks like narrative or chapter content, classify as translate — the story content takes priority over any surrounding non-content.
Sub-step 4: Build classification result
Build a JSON array of objects, one per entry in the same order as the snippets array. Each object has:
title -- the detected_title from the snippetaction -- one of "translate", "skip", or "uncertain"reason -- short explanation (e.g., "story content", "copyright page", "unclear from title alone")Sub-step 5: Present classification to user
Display the classification in two sections:
CHAPTERS TO TRANSLATE (N):
1. chapter-01.xhtml -- "Chapter 1: The Beginning" -- translate: story content
2. chapter-02.xhtml -- "Interlude" -- uncertain: unclear from title alone (?)
...
SKIPPED ENTRIES (N):
- chapter-15.xhtml -- "Copyright" -- skip: copyright page
- chapter-16.xhtml -- "Also By" -- skip: promotional content
...Rules:
(?) appended at the endoutput_filename -- "detected_title" -- action: reasonSub-step 6: Ask for confirmation
Ask the user:
"Does this classification look correct? (yes / adjust)"
Sub-step 7: Handle adjustment (if user says "adjust")
If the user wants to adjust:
1. chapter-01.xhtml -- "Chapter 1" [translate]
2. chapter-02.xhtml -- "Copyright" [skip]
3. chapter-03.xhtml -- "Interlude" [uncertain]Sub-step 8: Update metadata.json
After the user approves the classification, update the workspace by running this Python script via Bash. Replace <workspace_path> with the actual workspace path and <classification_json> with the JSON-encoded classification array (the result from Sub-step 4, with any adjustments from Sub-step 7 applied).
python -c "
import json, os, shutil
workspace = '<workspace_path>'
classification = json.loads('<classification_json>')
# Load metadata
meta_path = os.path.join(workspace, 'metadata.json')
with open(meta_path, 'r', encoding='utf-8') as f:
metadata = json.load(f)
# Build classification lookup by title
class_by_title = {}
for entry in classification:
class_by_title[entry['title']] = entry['action']
# Separate kept and skipped chapters
kept = []
for ch in metadata['chapters']:
action = class_by_title.get(ch['detected_title'], 'translate')
if action != 'skip':
kept.append(ch)
source_dir = os.path.join(workspace, 'source')
# Two-pass rename to avoid collisions
# Pass 1: rename kept files to temporary names
for i, ch in enumerate(kept, 1):
old_path = os.path.join(source_dir, ch['output_filename'])
tmp_name = f'_tmp_{i:02d}.xhtml'
tmp_path = os.path.join(source_dir, tmp_name)
if os.path.isfile(old_path):
shutil.move(old_path, tmp_path)
ch['_tmp_name'] = tmp_name
# Pass 2: rename temp files to final sequential names
for i, ch in enumerate(kept, 1):
tmp_path = os.path.join(source_dir, ch['_tmp_name'])
new_name = f'chapter-{i:02d}.xhtml'
new_path = os.path.join(source_dir, new_name)
if os.path.isfile(tmp_path):
shutil.move(tmp_path, new_path)
ch['sequence'] = i
ch['output_filename'] = new_name
del ch['_tmp_name']
# Recalculate total tokens
total_tokens = sum(ch.get('token_count', 0) for ch in kept)
# Update metadata
metadata['chapters'] = kept
metadata['total_tokens'] = total_tokens
with open(meta_path, 'w', encoding='utf-8') as f:
json.dump(metadata, f, indent=2, ensure_ascii=False)
print(f'Updated metadata.json: {len(kept)} chapters kept, renumbered 1..{len(kept)}')
print(f'Total tokens: {total_tokens}')
"IMPORTANT: When constructing the <classification_json> string for the Python script, escape any single quotes in titles by replacing ' with '"'"' so the shell command does not break. Alternatively, write the classification JSON to a temporary file and read it from Python.
Sub-step 9: Regenerate detection report
Regenerate detection_report.txt with the final two-section classification format by calling _write_detection_report from extract_epub.py.
IMPORTANT: Since Sub-step 8 already removed skipped entries from metadata.json, the report needs the ORIGINAL chapter list (before filtering) so it can show both kept and skipped sections. Save the original chapters list before Sub-step 8 runs, and use it here.
Replace <workspace_path>, <original_chapters_json> (ALL chapters from before Sub-step 8), and <classification_json> (the final classification array with any user adjustments):
python -c "
import json, sys
sys.path.insert(0, '${CLAUDE_PLUGIN_ROOT}/scripts')
from extract_epub import _write_detection_report
workspace = '<workspace_path>'
# original_chapters contains ALL chapters (before skip removal in Sub-step 8)
original_chapters = json.loads('<original_chapters_json>')
classification = json.loads('<classification_json>')
# Read metadata just for the title
meta_path = workspace + '/metadata.json'
with open(meta_path, 'r', encoding='utf-8') as f:
metadata = json.load(f)
_write_detection_report(workspace, metadata['title'], original_chapters, classification=classification)
print('detection_report.txt regenerated with classification')
"Sub-step 10: Proceed to Step 3.
Show a summary of the prepared workspace:
Workspace Ready!
Book title: <title from JSON output>
Genre: <selected genre>
Source language: <source language>
Target language: <target language>
Chapters detected: <total entries from extract_snippets> (from <TOC|spine>)
Chapters to translate: <count of kept chapters after classification>
Entries skipped: <count of skipped entries after classification>
Workspace path: <workspace_path from JSON output>
Round-trip test: PassedWhere (from TOC) or (from spine) reflects the translation_mode from metadata.json.
If reuse was used (i.e., reuse_from is set), add one more line after the summary block:
Reused from: <previous_workspace_path>Then print:
Review the detection_report.txt to see which chapters will be translated and which were skipped.
---
Next step: /build-book-glossary {workspace_path}Where {workspace_path} is the actual workspace_path value from the JSON output in Step 2.
IMPORTANT: Print the command exactly as /build-book-glossary, NOT /cabt:build-book-glossary. Do NOT add a namespace prefix. The short form is required for autocomplete to work.
ebooklib and lxml packages installedpip install ebooklib lxml20260324_1902_Apocalypse-Generic-System) and created in the current working directory. This makes each workspace unique and chronologically sortable.source/ (extracted chapter XHTML files), output/<target_language>/ (empty, ready for translated chapters), metadata.json (book and configuration details), test-roundtrip.epub (kept for visual inspection in Calibre), and detection_report.txt (chapter detection details).metadata.json file tracks processing state. After prepare-book, the status is "prepared". Downstream commands (/build-book-glossary, /translate-book) advance the state./translate-book starts, so you can open it in any e-reader to verify extraction quality.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.