Silencing the Ghostty bell when Claude Code asks a question
If you run Claude Code in Ghostty, you’ve probably heard it: a small chime every time Claude finishes a task or stops to ask you something. It is useful on the first day and maddening by the third, especially in a shared office or on a call. The obvious fix is to turn off Ghostty’s bell, and the obvious test is to ring the bell and listen, but on macOS both of those can pass while the chime carries on, because the chime usually isn’t the bell at all. Everything below was tested against Ghostty 1.3.1 on macOS.
1. Two ways a terminal program can get your attention
Claude Code doesn’t play a sound itself. It sends an escape sequence to the terminal and lets the terminal decide what to do, and there are two families of sequence it can send, depending on its notification setting.
The first is the terminal bell, the ancient BEL control character (0x07) that once rang an actual bell on a teletype. Ghostty handles it through the bell-features key, which can bounce the dock icon, add a 🔔 to the window title, flash a border, play the macOS system alert sound (system) or play a custom sound file (audio, on macOS since 1.3.0).
The second is a desktop notification, sent as OSC 9 or OSC 777. Ghostty passes these to macOS Notification Center as a normal notification from the Ghostty app, and macOS plays its notification sound unless you’ve turned that off for Ghostty. The bell-features key has no say over this path at all; it is governed by a separate key, desktop-notifications, which is on by default.
That split matters because Ghostty’s default bell on macOS is already silent. The default value is no-system,no-audio,attention,title,no-border: a dock bounce and a title marker, with both sound features off. If you hear a chime when Claude Code wants you, the likeliest source is a desktop notification, and silencing the bell will do nothing about it.
2. Fix one: tell Claude Code to stop notifying
Claude Code has a notification channel setting in ~/.claude/settings.json:
{ "preferredNotifChannel": "notifications_disabled" }That stops Claude Code sending either the bell or a desktop notification, which is the cleanest fix if Claude Code is the only thing that bothers you. Other tools keep their behaviour, so make finishing a long build, a background rsync and a shell script that pings you all still work.
It doesn’t cover anything else that runs inside the session. Claude Code hooks can emit their own terminal sequences, so a hook that sends an OSC 777 notification will still chime even with this setting in place; if you’ve written hooks that alert you, check them too.
3. Fix two: tell Ghostty to ignore both paths
The Ghostty side lives in ~/.config/ghostty/config, and there are two keys to set, not one:
bell-features = no-system,no-audio,no-attention,no-title,no-border
desktop-notifications = falseBoth halves of the bell-features line are deliberate. The key takes a comma separated list, and each feature can be switched on by name or off with a no- prefix, but any feature you don’t mention keeps its default. Writing bell-features = title therefore gives you the title marker and the dock bounce, because attention is on by default and you didn’t say otherwise. Writing bell-features = with nothing after it resets the key to its default. Neither is a way to say “off”, so name all five and set each one explicitly. (bell-features = false also switches everything off, but the explicit list is easier to read and to edit later.)
If you want to keep a quiet cue, spell it out the same way:
bell-features = no-system,no-audio,no-attention,title,no-borderThe available features are system, audio, attention (dock bounce on macOS), title (the 🔔 marker) and border. title is a nice middle ground, because you get a visual nudge on the tab and no noise.
Setting desktop-notifications = false stops every program in every Ghostty window from posting notifications, not just Claude Code. If you’d rather keep the banners and lose only the sound, leave it on and turn off “Play sound for notifications” for Ghostty in System Settings > Notifications instead.
Reload the config with Cmd+Shift+,, which needs no restart but only reloads the window you’re in. Every other open Ghostty window keeps running on the old config until you reload it too. Quitting and relaunching Ghostty (Cmd+Q, then reopen) reloads every window at once, which is simpler if you’ve got several open.
4. A small script to flip it
I change this depending on where I’m working, silent in the office and dock bounce at home, so it lives in a script rather than in my fingers.
A script that edits config should tell you what it changed in words you can check at a glance, and it should check its own work rather than trusting it. This one writes both keys explicitly, rejects feature names Ghostty wouldn’t recognise, runs ghostty +validate-config on the result and restores the backup if Ghostty rejects it, and then asks Ghostty what value it actually resolves from the config:
cat > ghostty-bell <<'EOF'
#!/usr/bin/env bash
# ghostty-bell: control every way Ghostty can get your attention.
# Usage: ghostty-bell [off|<feature,feature,...>]
# bell features: system, audio, attention, title, border
# notify: allow desktop notifications (OSC 9 / OSC 777)
# Anything you don't name is switched off explicitly. Ghostty keeps its
# default for any bell feature left unlisted, and an empty value resets
# the whole key to its default, so neither is a safe way to say "off".
set -euo pipefail
xdg="${XDG_CONFIG_HOME:-$HOME/.config}"
cfg="${GHOSTTY_CONFIG:-$xdg/ghostty/config}"
want="${1-off}"
[ "$want" = "off" ] && want=""
want=$(printf '%s' "$want" | tr -d ' ')
all="system audio attention title border"
# Find the ghostty binary, if there is one
G=""
for c in ghostty /Applications/Ghostty.app/Contents/MacOS/ghostty; do
if command -v "$c" >/dev/null 2>&1; then G=$(command -v "$c"); break; fi
done
# Reject anything that is not a real feature name
for f in $(printf '%s' "$want" | tr ',' ' '); do
case " $all notify " in
*" $f "*) ;;
*) echo "unknown feature: $f (valid: system, audio, attention, title, border, notify)" >&2; exit 1 ;;
esac
done
# Explicit values for both keys
val=""
for f in $all; do
case ",$want," in *",$f,"*) item="$f" ;; *) item="no-$f" ;; esac
val="${val:+$val,}$item"
done
case ",$want," in *",notify,"*) notify=true ;; *) notify=false ;; esac
label() {
case "$1" in
audio) echo "custom sound" ;;
system) echo "system alert sound" ;;
attention) echo "dock bounce" ;;
title) echo "title marker" ;;
border) echo "border flash" ;;
*) echo "$1" ;;
esac
}
# Ghostty's own default, so "unset" can be described truthfully
default_bell="no-system,no-audio,attention,title,no-border"
if [ -n "$G" ]; then
d=$("$G" +show-config --default 2>/dev/null | sed -n 's/^bell-features = //p')
[ -n "$d" ] && default_bell="$d"
fi
describe_bell() {
local v t out="" suffix=""
v=$(printf '%s' "$1" | tr -d ' ')
case "$v" in
""|unset) v="$default_bell"; suffix=" (Ghostty default)" ;;
false) v="no-system,no-audio,no-attention,no-title,no-border" ;;
true) v="system,audio,attention,title,border" ;;
esac
for t in $(printf '%s' "$v" | tr ',' ' '); do
case "$t" in no-*) continue ;; esac
out="${out:+$out, }$(label "$t")"
done
printf '%s%s' "${out:-nothing}" "$suffix"
}
describe_notify() {
case "$(printf '%s' "$1" | tr -d ' ')" in
false) printf 'off' ;;
true) printf 'on' ;;
*) printf 'on (Ghostty default)' ;;
esac
}
current() {
grep "^[[:space:]]*$1[[:space:]]*=" "$cfg" 2>/dev/null | tail -n1 | sed 's/^[^=]*=[[:space:]]*//' || true
}
set_key() {
local key="$1" line="$1 = $2"
if grep -q "^[[:space:]]*$key[[:space:]]*=" "$cfg"; then
sed -i.tmp "s|^[[:space:]]*$key[[:space:]]*=.*|$line|" "$cfg" && rm -f "$cfg.tmp"
else
printf '%s\n' "$line" >> "$cfg"
fi
}
mkdir -p "$(dirname "$cfg")"; touch "$cfg"
was_bell=$(current bell-features); grep -q '^[[:space:]]*bell-features' "$cfg" || was_bell=unset
was_notify=$(current desktop-notifications); grep -q '^[[:space:]]*desktop-notifications' "$cfg" || was_notify=unset
cp "$cfg" "$cfg.bak"
set_key bell-features "$val"
set_key desktop-notifications "$notify"
# Refuse to leave behind a config Ghostty cannot parse
if [ -n "$G" ]; then
if ! errs=$("$G" +validate-config --config-file="$cfg" 2>&1) || [ -n "$errs" ]; then
cp "$cfg.bak" "$cfg"
echo "Ghostty rejected the new config, original restored:" >&2
printf '%s\n' "$errs" >&2
exit 1
fi
fi
# What Ghostty resolves from its default config location (skipped when
# GHOSTTY_CONFIG points somewhere Ghostty would not look on its own)
resolved=""
if [ -n "$G" ] && [ -z "${GHOSTTY_CONFIG:-}" ]; then
resolved=$("$G" +show-config 2>/dev/null | sed -n 's/^bell-features = //p')
[ -n "$resolved" ] || resolved="$default_bell (default)"
fi
if [ -t 1 ]; then b=$(tput bold); dim=$(tput dim); g=$(tput setaf 2); r=$(tput sgr0)
else b=""; dim=""; g=""; r=""; fi
same() { [ "$(printf '%s' "$1" | tr -d ' ')" = "$2" ]; }
echo
if same "$was_bell" "$val" && same "$was_notify" "$notify"; then
echo " ${b}Ghostty bell unchanged${r}"
else
echo " ${g}${b}Ghostty bell updated${r}"
fi
printf ' %sbell%s %s -> %s%s%s\n' "$dim" "$r" "$(describe_bell "$was_bell")" "$b" "$(describe_bell "$val")" "$r"
printf ' %snotifications%s %s -> %s%s%s\n' "$dim" "$r" "$(describe_notify "$was_notify")" "$b" "$(describe_notify "$notify")" "$r"
echo
printf ' %sconfig%s %s\n' "$dim" "$r" "$cfg"
printf ' %sbackup%s %s\n' "$dim" "$r" "$cfg.bak"
[ -n "$G" ] && printf ' %svalidated%s ghostty +validate-config passed\n' "$dim" "$r"
[ -n "$resolved" ] && printf ' %sresolves%s bell-features = %s\n' "$dim" "$r" "$resolved"
echo
echo " Open windows keep their old settings until reloaded: Cmd+Shift+, in each one, or Cmd+Q and relaunch."
echo
EOF
chmod +x ghostty-bellIt rewrites each line if it exists and appends it if it doesn’t, keeps a .bak each time, says unchanged when nothing moved, and strips the colour codes when output isn’t a terminal so that piping it stays clean. When a key is missing or empty, it describes Ghostty’s real default rather than calling it silent, which is the mistake that makes an empty value so easy to trust.
5. Calling it
The argument is the list of things you want switched on, and the script turns everything else off explicitly. notify is the one extra word: it allows desktop notifications, which are otherwise switched off. off, or no argument at all, turns everything off.
./ghostty-bell off # total silence: no bell of any kind, no notifications
./ghostty-bell # same thing, because off is the default
./ghostty-bell title # quiet, but the tab gets a 🔔 so you notice on return
./ghostty-bell attention # no sound, dock icon bounces (macOS)
./ghostty-bell title,attention # both visual cues, still no audio
./ghostty-bell title,notify # title marker plus desktop notification banners
./ghostty-bell system # the macOS alert sound back onTo target a different config file, set GHOSTTY_CONFIG; the script still validates the file, but skips the resolve check because Ghostty wouldn’t read that file on its own:
GHOSTTY_CONFIG=~/work/ghostty.conf ./ghostty-bell offEach run prints the before and after. Here it is fixing a config that had the empty bell-features = line:
$ ./ghostty-bell off
Ghostty bell updated
bell dock bounce, title marker (Ghostty default) -> nothing
notifications on (Ghostty default) -> off
config /Users/you/.config/ghostty/config
backup /Users/you/.config/ghostty/config.bak
validated ghostty +validate-config passed
resolves bell-features = no-system,no-audio,no-attention,no-title,no-border
Open windows keep their old settings until reloaded: Cmd+Shift+, in each one, or Cmd+Q and relaunch.Run it twice and the second run says Ghostty bell unchanged, which is the tell that you already did this and forgot. A typo such as ./ghostty-bell titel stops with an error and leaves the file alone.
6. Testing it properly
The test I used to recommend was printf '\a', and silence meant success. That test is valid in the narrow sense that it rings the bell, but it only exercises one of the two paths, and on macOS it is silent with Ghostty’s default settings anyway. It passes before you’ve changed anything, so it can’t tell you whether a fix worked, and it says nothing about desktop notifications, which is where the chime usually comes from.
A useful test fires each path separately and asks you what you heard. It also waits a few seconds before each one, so you can switch to another app first, because Ghostty only bounces the dock when it is unfocused and that is also the situation you care about when Claude Code wants you:
cat > ghostty-bell-test <<'EOF'
#!/usr/bin/env bash
# ghostty-bell-test: fire each thing that can make Ghostty chime, one at a
# time, so you can hear which one is responsible. Run it inside Ghostty.
set -uo pipefail
if [ "${TERM_PROGRAM:-}" != "ghostty" ]; then
echo "Run this inside a Ghostty window (TERM_PROGRAM is '${TERM_PROGRAM:-unset}')." >&2
exit 1
fi
names=("Terminal bell (BEL)" "Desktop notification (OSC 9)" "Desktop notification (OSC 777)")
seqs=($'\a' $'\e]9;ghostty-bell-test: OSC 9\e\\' $'\e]777;notify;ghostty-bell-test;OSC 777\e\\')
results=()
echo
echo " Each test waits 5 seconds so you can switch to another app first;"
echo " Ghostty only bounces the dock and posts some notifications when unfocused."
for i in 0 1 2; do
echo
printf ' %d/3 %s: switch away now, firing in ' "$((i+1))" "${names[$i]}"
for s in 5 4 3 2 1; do printf '%s ' "$s"; sleep 1; done
printf '%s' "${seqs[$i]}" > /dev/tty
sleep 2
printf '\n Did you hear a sound? [y/N] '
read -r ans
case "$ans" in [yY]*) results+=("sound") ;; *) results+=("silent") ;; esac
done
echo
echo " Results"
for i in 0 1 2; do printf ' %-32s %s\n' "${names[$i]}" "${results[$i]}"; done
echo
if [ "${results[0]}" = "sound" ]; then
echo " The bell itself makes noise: system or audio is on in bell-features."
echo " Fix: ghostty-bell off (or keep only the visual features you want)."
fi
if [ "${results[1]}" = "sound" ] || [ "${results[2]}" = "sound" ]; then
echo " Desktop notifications make noise, and bell-features does not control them."
echo " Fix: desktop-notifications = false in the Ghostty config (ghostty-bell off does this),"
echo " or turn off 'Play sound for notifications' for Ghostty in System Settings > Notifications."
fi
if [ "${results[*]}" = "silent silent silent" ]; then
echo " Ghostty is silent on every path. If Claude Code still chimes, the sound is"
echo " coming from outside the terminal, such as a hook script or another app."
fi
echo
EOF
chmod +x ghostty-bell-testRun it inside a Ghostty window, after reloading:
./ghostty-bell-testThe summary names the path that made the noise and the setting that controls it. If all three are silent and Claude Code still chimes, the sound is coming from outside the terminal, and a hook script that sends its own notification is the first thing to check.
Two other checks are worth knowing. To see what Ghostty resolves from your config, run ghostty +show-config | grep bell-features, using /Applications/Ghostty.app/Contents/MacOS/ghostty if ghostty isn’t on your path. It prints only settings that differ from the default, so no output means you are running the defaults. And if the result surprises you, check for a second config file: on macOS Ghostty also reads ~/Library/Application Support/com.mitchellh.ghostty/config, and if both exist it’s easy to spend ten minutes editing the wrong one.
7. The bell was never the whole problem
Quietening Ghostty stops Claude Code’s own noise, but it doesn’t touch the rest of macOS. Slack, Mail, Messages, Calendar and every other app with a notification sound keep going, and none of them read ~/.config/ghostty/config, because that noise comes from a different layer entirely: Notification Center, not the terminal.
macOS splits this into two halves, and only one of them is safely scriptable. The global half, meaning the “Play sound effects” toggle, the volume change feedback beep and the system alert volume, lives in NSGlobalDomain, and defaults write can flip it without asking permission.
The per app half, whether Mail or Slack plays a sound for its own notifications, has no supported scripting interface. Apple keeps those settings in Notification Center’s own preferences and expects you to change them by clicking in System Settings, which is a sensible protection against any process silently disabling your alerts.
The same lesson from the ghostty-bell script applies here: don’t just say “done”, say what changed. defaults write gives no feedback of its own, so this version reads each setting before it writes and reports was → now for every one, the same way ghostty-bell reports its before and after:
cat > mute_notifications.sh <<'EOF'
#!/bin/bash
#
# mute_notifications.sh
# Review and silence notification sounds on macOS.
#
# What this script CAN safely automate:
# - Mute system UI sound effects (the "Play sound effects" toggle)
# - Mute the volume-change feedback sound
# - Mute the system alert/beep volume
#
# What macOS does NOT let a script change (Apple offers no supported
# way to script per-app notification settings; they are changed by a
# human clicking in System Settings):
# - Per-app "Allow sound" toggles for Mail, Messages, Slack, etc.
#
# For that part, this script opens the right settings pane and prints
# a checklist so you can knock it out in under a minute.
#
# It reports what it actually changed (was -> now for each setting),
# not just that it ran.
set -euo pipefail
if [ -t 1 ]; then b=$(tput bold); dim=$(tput dim); g=$(tput setaf 2); r=$(tput sgr0)
else b=""; dim=""; g=""; r=""; fi
onoff() { [ "$1" = "0" ] && echo "off" || echo "on"; }
read_default() {
defaults read NSGlobalDomain "$1" 2>/dev/null || echo "unset"
}
echo
echo " ${b}macOS Notification Sound Cleanup${r}"
echo
# --- capture "was" state before changing anything ---
was_ui=$(read_default com.apple.sound.uiaudio.enabled)
was_feedback=$(read_default com.apple.sound.beep.feedback)
was_volume=$(read_default com.apple.sound.beep.volume)
# --- apply changes ---
defaults write NSGlobalDomain com.apple.sound.uiaudio.enabled -int 0
defaults write NSGlobalDomain com.apple.sound.beep.feedback -int 0
defaults write NSGlobalDomain com.apple.sound.beep.volume -float 0.0
# Restart the settings daemon so changes take effect without a full logout
killall SystemUIServer 2>/dev/null || true
# --- capture "now" state and report was -> now for each ---
now_ui=$(read_default com.apple.sound.uiaudio.enabled)
now_feedback=$(read_default com.apple.sound.beep.feedback)
now_volume=$(read_default com.apple.sound.beep.volume)
report_bool() {
local label="$1" was="$2" now="$3"
local was_l now_l
[ "$was" = "unset" ] && was_l="on (default)" || was_l="$(onoff "$was")"
now_l="$(onoff "$now")"
if [ "$was_l" = "$now_l" ]; then
printf ' %s%-28s%s %sunchanged%s (%s)\n' "$dim" "$label" "$r" "$dim" "$r" "$now_l"
else
printf ' %s%-28s%s %s%s%s -> %s%s%s\n' "$dim" "$label" "$r" "$dim" "$was_l" "$r" "$g$b" "$now_l" "$r"
fi
}
echo " ${b}${g}Changes applied${r}"
report_bool "UI sound effects" "$was_ui" "$now_ui"
report_bool "Volume-change feedback" "$was_feedback" "$now_feedback"
echo " ${dim}Alert/beep volume${r} ${dim}${was_volume:-unset}${r} -> ${g}${b}0.0 (muted)${r}"
echo
echo " ${dim}These persist until you change them back; no restart needed.${r}"
echo
# Open Notification settings for the manual per-app review
echo " Opening System Settings > Notifications for the manual per-app review..."
open "x-apple.systempreferences:com.apple.preference.notifications"
cat <<INNER
------------------------------------------------------------
Manual checklist (Apple requires a click here, not a script):
------------------------------------------------------------
In System Settings > Notifications, click each app you want to
quiet and turn OFF "Play sound for notifications". Common noisy
ones to check first:
[ ] Mail
[ ] Messages
[ ] Calendar
[ ] Reminders
[ ] FaceTime
[ ] Slack / Teams / Zoom (if installed)
[ ] App Store
[ ] News
Tip: You can also select an app and toggle "Allow Notifications"
off entirely if you don't want banners either, not just sound.
For a temporary blanket mute instead of per-app changes, turn on
a Focus mode (Control Center > Focus); it silences all sounds
and banners until you turn it off.
------------------------------------------------------------
To verify the global mute worked, trigger a volume change and
listen for the beep:
osascript -e 'set volume output volume 50'
No sound = feedback beep is off.
INNER
EOF
chmod +x mute_notifications.shIt needs no sudo and no restart. Run it:
./mute_notifications.shEach run tells you exactly what moved:
$ ./mute_notifications.sh
macOS Notification Sound Cleanup
Changes applied
UI sound effects on (default) -> off
Volume-change feedback on (default) -> off
Alert/beep volume unset -> 0.0 (muted)
These persist until you change them back; no restart needed.
Opening System Settings > Notifications for the manual per-app review...Run it a second time and the settings that already changed report unchanged (off) instead of pretending to flip them again, which is the same tell as ghostty-bell saying “unchanged”.
It mutes the three global sound settings immediately, then hands you off to System Settings already open on the right pane, with the checklist printed right there in the terminal so you’re not hunting for what to click next.
Verifying it worked
The global part is quick to check: trigger a volume change and listen for the beep.
osascript -e 'set volume output volume 50'No sound means the feedback beep is off. The per app part you verify the obvious way, by sending yourself a test message in each app you just muted and confirming it arrives silently.
Combined with the Ghostty fix above, that’s the full stack quiet: Claude Code’s own bell, the terminal’s handling of it, and the rest of the OS underneath.