Getting it into your agent
One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.
npx agentmods add commands/chendrizzy/claude-tts/loggit clone --depth 1 https://github.com/chendrizzy/claude-ttsWrote this? Show the measurements
A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.
[](https://agentmods.dev/commands/chendrizzy/claude-tts/log)<a href="https://agentmods.dev/commands/chendrizzy/claude-tts/log"><img src="https://agentmods.dev/badge/commands/chendrizzy/claude-tts/log.svg" alt="Measured on agentmods" height="20"></a>What it costs to keep this loaded
Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5 | $0.00025 | $0.02079 |
| Opus 5 | $0.00013 | $0.01040 |
| Sonnet 5 | $0.00005 | $0.00416 |
| Haiku 4.5 | $0.00003 | $0.00208 |
Grade A, and why
tts:log scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 5d ago.
A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.
Nothing flagged
None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.
How it starts
The opening of the file, as written. The whole thing — 203 lines — stays where its author put it; the contents beside it link to each section on GitHub.
/tts:log — spoken-output log
Print what the TTS daemon has actually spoken, newest first, with timestamps and
category. Reads the per-session JSONL the daemon appends at
~/.claude/logs/tts/spoken/<session>.jsonl (written by daemon/spoken_log.py).
Optional argument: how many entries to show (default 25). Pass a session id with
--session <id> to target a specific session instead of the most-recently-active.
When statusline.include_subagent_in_main is enabled in config.json and no
--session is given, the log shows a MERGED, sub-agent-aware view: lines spoken
by sibling sub-agents / background agents during this session's span are folded
in, each tagged by source (spoken_log.read_merged()). Default is off — the
plain single-session view.
Run:
ARGS="$ARGUMENTS"
N=25; SESSION=""
# parse: optional count and/or --session <id>
set -- $ARGS
while [ $# -gt 0 ]; do
case "$1" in
--session) SESSION="$2"; shift 2;;
''|*[!0-9]*) shift;;
*) N="$1"; shift;;
esac
done
DIR="$HOME/.claude/logs/tts/spoken"
if [ -n "$SESSION" ]; then
F="$DIR/$(printf '%s' "$SESSION" | tr -c 'A-Za-z0-9_-' '_').jsonl"
else
F=$(ls -t "$DIR"/*.jsonl 2>/dev/null | head -1)
fi
python3 - "$F" "$N" "$SESSION" <<'PY'
# Mirrors daemon/spoken_log.read_merged (the tested reference) — kept inline so
# the command stays self-contained as an installed plugin.
import sys, json, time, os, glob, hashlib
f = sys.argv[1] if len(sys.argv) > 1 else ""
try:
n = int(sys.argv[2])
except Exception:
n = 25
explicit_session = bool(sys.argv[3].strip()) if len(sys.argv) > 3 else False
DIR = os.path.expanduser("~/.claude/logs/tts/spoken")
def read_all(path):
out = []
try:
with open(path, encoding="utf-8", errors="ignore") as fh:
for ln in fh:
ln = ln.strip()
if ln:
try: out.append(json.loads(ln))
except Exception: pass
except OSError:
pass
return out
def include_subagent_flag():
cands = []
if os.environ.get("CLAUDE_TTS_CONFIG"):
cands.append(os.environ["CLAUDE_TTS_CONFIG"])
# Canonical config location — matches daemon/paths.py config_path(), honoring
# XDG_CONFIG_HOME (else ~/.config/claude-tts/config.json). Checked FIRST so the
# include_subagent_in_main flag is actually read for a standard install.
xdg = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
cands.append(os.path.join(xdg, "claude-tts", "config.json"))
cands.append(os.path.expanduser("~/.claude/tts/config/config.json")) # legacy fallback
cands.append(os.path.join(os.getcwd(), "config.json"))
for c in cands:
try:
with open(c, encoding="utf-8") as fh:
sl = (json.load(fh) or {}).get("statusline", {})
return bool(sl.get("include_subagent_in_main", False))
except Exception:
continue
return False
# --- G006: per-line "project · voice" tag -------------------------------------
# Reproduces daemon/content_router.py's mapping EXACTLY (project_root ->
# project_label -> resolve_project_voice), pinned to the daemon by the source
# tree's tests/test_project_voice_parity.py. The tag shows which voice the daemon
# speaks each entry's repo in — the audible cue, made visible.
def load_pool():
cands = []
if os.environ.get("CLAUDE_TTS_CONFIG"):
cands.append(os.environ["CLAUDE_TTS_CONFIG"])
xdg = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
cands.append(os.path.join(xdg, "claude-tts", "config.json"))
cands.append(os.path.expanduser("~/.claude/tts/config/tts_user_config.json"))
cands.append(os.path.expanduser("~/.claude/tts/config/config.json"))
cands.append(os.path.join(os.getcwd(), "config.json"))
for c in cands:
try:
with open(c, encoding="utf-8") as fh:
pv = (json.load(fh) or {}).get("project_voices", {}) or {}
if pv.get("enabled") and pv.get("pool"):
return list(pv["pool"])
except Exception:
continue
return []
_POOL = load_pool()
def _proj_root(cwd):
if not isinstance(cwd, str) or not cwd.strip():
return ""
cwd = cwd.strip()
root = cwd
try:
d = os.path.abspath(cwd)
while True:
if os.path.exists(os.path.join(d, ".git")):
root = d
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
except Exception:
root = cwd
return root
def _proj_label(cwd):
if not isinstance(cwd, str):
return ""
c = cwd.strip()
if not c:
return ""
proj = c.rstrip("/").rsplit("/", 1)[-1]
if not proj or proj in ("", "/", ".", "tmp"):
return ""
return proj
def pv_tag(rec):
"""'project · voice' for a spoken-log record, or '-' when unresolvable."""
lbl = _proj_label(_proj_root(rec.get("cwd") or ""))
if not lbl:
return "-"
if not _POOL:
return lbl
idx = int(hashlib.sha256(lbl.encode("utf-8")).hexdigest(), 16) % len(_POOL)
return f"{lbl}·{_POOL[idx]}"
if not f:
print("(no spoken output logged yet — speak something, then try again)")
raise SystemExit
# include_subagent_in_main: merge sibling-agent lines spoken during this
# (anchor) session's span. Only when no explicit --session was requested.
if include_subagent_flag() and not explicit_session:
main = [dict(r, session="main") for r in read_all(f)]
lower = min((r.get("ts", 0) for r in main), default=0)
# Same-project gate (matches daemon/spoken_log.read_merged): only fold in
# siblings whose entries share this session's cwd. Sub-agents inherit the
# parent's cwd; an unrelated concurrent session has a different one — without
# this gate two sessions in different dirs would mirror each other. Derive
# cwd from this session's newest entry that recorded one; None → legacy
# time-only merge (entries predating the cwd field).
cur_cwd = next((r.get("cwd") for r in reversed(main) if r.get("cwd")), None)
recs = list(main)
anchor = os.path.abspath(f)
for sib in glob.glob(os.path.join(DIR, "*.jsonl")):
if os.path.abspath(sib) == anchor:
continue
tag = os.path.basename(sib)[:-6][:8]
for r in read_all(sib):
if r.get("ts", 0) < lower:
continue
if cur_cwd is not None and r.get("cwd") != cur_cwd:
continue # different project — exclude (the cross-session fix)
recs.append(dict(r, session=tag))
recs.sort(key=lambda r: r.get("ts", 0), reverse=True)
shown = recs[:n]
if not shown:
print("(no spoken output logged yet)"); raise SystemExit
print(f"# spoken log — MERGED (sub-agent aware) ({len(shown)} of {len(recs)} entries)")
for r in shown:
ts = time.strftime("%H:%M:%S", time.localtime(r.get("ts", 0)))
cat = r.get("category") or "-"
src = r.get("session", "-")
print(f"{ts} [{src:>8}] [{cat:>12}] {pv_tag(r):<20} {r.get('text','')}")
else:
lines = read_all(f)
shown = lines[-n:]
if not shown:
print("(no spoken output logged yet)"); raise SystemExit
print(f"# spoken log — {os.path.basename(f)} ({len(shown)} of {len(lines)} entries)")
for r in reversed(shown): # newest first
ts = time.strftime("%H:%M:%S", time.localtime(r.get("ts", 0)))
cat = r.get("category") or "-"
print(f"{ts} [{cat:>12}] {pv_tag(r):<20} {r.get('text','')}")
PY
What this file has done since we first saw it
Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.
- 5d ago First seen · 203 lines · 25 tokens per session scan A 937a47d171db
tts:log is a command published in the GitHub repository chendrizzy/claude-tts (1 stars, last pushed 19d ago), licensed MIT. It adds 25 tokens to every session and 2,079 once invoked, about $0.0001 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-31.
Other commands, from other repositories
open
SoDam-Design-Kit open 대시보드 — 검증 이력·판정서·스크린샷을 브라우저로 열람 + 재검증.
wp-seed
Seed WordPress content from demo HTML — parses sections, creates pages, imports media, populates ACF fields, builds menus, supports bilingual content.
setup
SoDam-Design-Kit 설정 마법사 — config.json 생성 + shadcn 컴포넌트 스캔으로 component-map 초기 시드.
wp-header
Build the WordPress header — responsive nav, logo, language switcher, WP menu system integration.
wp-audit
Comprehensive audit — security, SEO, accessibility, performance, best practices.
wp-cpt
Custom post type builder — registers a CPT and generates its fields, archive, single, optional teaser query-section, and seed helper.