#!/bin/bash # # Removes what Tenfold added to your Mac, for when the app has already been # deleted. If Tenfold is still installed, use Settings > Integrations > # Uninstall integration instead. # # curl -fsSL https://gettenfold.app/uninstall.sh | bash # # What it does: # - removes Tenfold's hooks from ~/.claude/settings.json, keeping yours # - puts your original Claude Code status line back # - removes Tenfold's hooks from Codex's hooks.json, if it has any # - deletes Tenfold's scripts, snapshots and event files # # Any file it changes is copied first, next to the original. It doesn't touch # your worktrees, your other settings or settings.json.tenfold-backup. If a # settings file isn't valid JSON, it stops without changing anything. # # Uses only what ships with macOS. set -u support="$HOME/Library/Application Support/Tenfold" claude_settings="$HOME/.claude/settings.json" codex_hooks="${CODEX_HOME:-$HOME/.codex}/hooks.json" # Tenfold rewrites these settings while it runs, so it has to be closed first. if /bin/ps -axo comm= | /usr/bin/grep -q '/Tenfold\.app/Contents/MacOS/Tenfold$'; then echo "Tenfold is running. Quit it, then run this again." >&2 exit 1 fi work=$(/usr/bin/mktemp -d) || exit 1 trap '/bin/rm -rf "$work"' EXIT # JavaScript for Automation: real JSON parsing, and it keeps your key order. /bin/cat > "$work/clean.js" <<'JS' ObjC.import('Foundation'); function read(path) { const text = $.NSString.stringWithContentsOfFileEncodingError(path, $.NSUTF8StringEncoding, null); return text.isNil() ? null : text.js; } function decode(base64) { const data = $.NSData.alloc.initWithBase64EncodedStringOptions(base64, 0); if (data.isNil()) return ''; const text = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding); return text.isNil() ? '' : text.js; } // Tenfold's status line command carries the original, base64-encoded, so it // can be recovered even if Tenfold's folder is gone. For a home folder with // unusual characters, the whole command is encoded once more. function embeddedOriginal(command) { const find = text => { const match = text.match(/printf %s "([A-Za-z0-9+\/=]*)"/); return match ? decode(match[1]).trim() : null; }; const direct = find(command); if (direct !== null) return direct; const outer = command.match(/printf %s ([A-Za-z0-9+\/=]+) \|/); return outer ? (find(decode(outer[1])) || '') : ''; } function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } function isOurs(handler) { return isObject(handler) && typeof handler.command === 'string' && /tenfold-(hook\.sh|snapshot\.pl|codex-hook\.sh)/.test(handler.command); } function run(argv) { const [path, out, support] = argv; const text = read(path); if (text === null) return 'invalid'; if (text.trim() === '') return 'unchanged'; let root; try { root = JSON.parse(text); } catch (e) { return 'invalid'; } if (!isObject(root)) return 'invalid'; let changed = false; if (isObject(root.hooks)) { for (const event of Object.keys(root.hooks)) { const groups = root.hooks[event]; if (!Array.isArray(groups)) continue; let touched = false; const kept = []; for (const group of groups) { if (!isObject(group) || !Array.isArray(group.hooks) || !group.hooks.some(isOurs)) { kept.push(group); continue; } touched = true; // Only Tenfold's handlers go, even in a group that also holds yours. group.hooks = group.hooks.filter(handler => !isOurs(handler)); if (group.hooks.length) kept.push(group); } if (!touched) continue; changed = true; if (kept.length) root.hooks[event] = kept; else delete root.hooks[event]; } if (changed && Object.keys(root.hooks).length === 0) delete root.hooks; } const line = root.statusLine; if (isObject(line) && typeof line.command === 'string' && line.command.includes('tenfold-statusline.sh')) { const saved = read(support + '/tenfold-statusline-original.txt'); const original = saved !== null ? saved.trim() : embeddedOriginal(line.command); if (original) { line.type = 'command'; line.command = original; } else { delete root.statusLine; } changed = true; } if (!changed) return 'unchanged'; const result = $.NSString.alloc.initWithUTF8String(JSON.stringify(root, null, 2) + '\n'); const ok = result.writeToFileAtomicallyEncodingError(out, true, $.NSUTF8StringEncoding, null); return ok ? 'changed' : 'failed'; } JS # First pass: work out every change, so nothing is touched if a file is bad. pending=0 check() { local file=$1 id=$2 target result [ -e "$file" ] || return 0 target=$(/bin/realpath "$file") || return 1 result=$(/usr/bin/osascript -l JavaScript "$work/clean.js" "$target" "$work/$id.json" "$support" 2>/dev/null) case "$result" in changed) echo "$target" > "$work/$id.target"; pending=$((pending + 1)) ;; unchanged) ;; invalid) echo "$file isn't valid JSON, so nothing was changed. Fix it, then run this again." >&2 return 1 ;; *) echo "Couldn't read $file, so nothing was changed." >&2 return 1 ;; esac } check "$claude_settings" claude || exit 1 check "$codex_hooks" codex || exit 1 # Second pass: back up, then replace. Writing to the resolved path keeps a # dotfiles symlink in place, and cp -p keeps the file's permissions. apply() { local id=$1 target backup temp [ -f "$work/$id.target" ] || return 0 target=$(/bin/cat "$work/$id.target") backup="$target.before-tenfold-uninstall-$(/bin/date +%Y%m%d%H%M%S)" temp="$target.tenfold-uninstall.$$" /bin/cp -p "$target" "$backup" || return 1 /bin/cp -p "$target" "$temp" || return 1 /bin/cat "$work/$id.json" > "$temp" || { /bin/rm -f "$temp"; return 1; } /bin/mv -f "$temp" "$target" || { /bin/rm -f "$temp"; return 1; } echo "Cleaned $target" echo " A copy of the old file is at $backup" } apply claude || { echo "Couldn't update $claude_settings. Nothing else was changed." >&2; exit 1; } apply codex || { echo "Couldn't update $codex_hooks. Tenfold's files were left in place." >&2; exit 1; } # Only now is it safe to delete the scripts those settings pointed at. if [ -d "$support" ]; then /bin/rm -f "$support/tenfold-hook.sh" "$support/tenfold-statusline.sh" \ "$support/tenfold-snapshot.pl" "$support/tenfold-codex-hook.sh" \ "$support/tenfold-statusline-original.txt" "$support/app.pid" /bin/rm -rf "$support/snapshots" "$support/inbox" "$support/replies" /bin/rmdir "$support" 2>/dev/null echo "Removed Tenfold's scripts from $support" fi if [ "$pending" -eq 0 ]; then echo "No Tenfold hooks or status line found in your settings." fi if [ -d /Applications/Tenfold.app ] || [ -d "$HOME/Applications/Tenfold.app" ]; then echo "Tenfold is still installed. Delete it, or it may add these back when it next opens." >&2 fi echo "Done. Restart any open Claude Code sessions."