Skip to content

Instantly share code, notes, and snippets.

@thmsmlr
Created August 5, 2025 05:25
Show Gist options
  • Save thmsmlr/97570bd4c517c2f7808c95051eecfa90 to your computer and use it in GitHub Desktop.
Save thmsmlr/97570bd4c517c2f7808c95051eecfa90 to your computer and use it in GitHub Desktop.
make Claude Code "Coding Agent Complete" when done working
#!/usr/bin/env bash
set -euo pipefail
### defaults ###############################################################
VOICE="alloy" # any voice listed in the docs, e.g. 'alloy' 'nova' …
SPEED=1.0 # 0.25 – 4.0 (1.0 = normal)
FORMAT="mp3" # mp3 | wav | flac …
VERBOSE=0
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/Library/Caches}/openai-say"
mkdir -p "$CACHE_DIR"
usage() {
cat <<EOF
Usage: openai-say [options] "text to speak"
echo "hi" | openai-say [options]
Options:
-v, --voice <voice> Voice name (default: $VOICE)
-s, --speed <float> Playback speed 0.25–4.0 (default: $SPEED)
-f, --format <fmt> Audio format: mp3, wav, flac (default: $FORMAT)
-x, --verbose Print what is happening
-h, --help Show this help
EOF
}
### option parsing #########################################################
TEXT=""
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--voice) VOICE="$2"; shift 2;;
-s|--speed) SPEED="$2"; shift 2;;
-f|--format) FORMAT="$2"; shift 2;;
-x|--verbose) VERBOSE=1; shift;;
-h|--help) usage; exit 0;;
--) shift; break;;
-*) echo "Unknown flag $1"; usage; exit 1;;
*) TEXT+=" $1"; shift;;
esac
done
# Accept text piped via STDIN if no arg string was given
if [[ -z "${TEXT// }" ]]; then
TEXT="$(cat)"
fi
[[ -z "${TEXT// }" ]] && { echo "No text supplied"; usage; exit 1; }
TEXT="${TEXT# }" # strip leading space
### cache filename #########################################################
HASH=$(printf '%s|%s|%s|%s' "$TEXT" "$VOICE" "$SPEED" "$FORMAT" | md5 -q)
OUTFILE="${CACHE_DIR}/${HASH}.${FORMAT}"
### use cache if available #################################################
if [[ -f "$OUTFILE" ]]; then
(( VERBOSE )) && echo "openai-say: using cached file $OUTFILE" >&2
else
(( VERBOSE )) && echo "openai-say: fetching from OpenAI" >&2
: "${OPENAI_API_KEY:?OPENAI_API_KEY not set}"
# shellcheck disable=SC2016
JSON=$(jq -n --arg txt "$TEXT" --arg v "$VOICE" --arg f "$FORMAT" --argjson sp "$SPEED" \
'{model:"gpt-4o-mini-tts",input:$txt,voice:$v,format:$f,speed:$sp}')
curl -sS https://api.openai.com/v1/audio/speech \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
--data "$JSON" \
--output "$OUTFILE"
fi
### play ###################################################################
afplay "$OUTFILE"
# ~/.claude/settings.json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "openai-say --speed 1.5 \"Coding agent complete\""
}
]
}
]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment