From 4cb96d534e77225b07aab02bad995274c3abaf98 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Wed, 29 Jul 2026 10:00:38 +0200 Subject: [PATCH 1/2] Allow WebSearch in the local Claude settings --- .claude/settings.local.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d42b1a9..13b5d63 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -122,7 +122,8 @@ "mcp__azure-devops__pipelines_get_run", "mcp__atlassian__getJiraIssue", "mcp__atlassian__searchJiraIssuesUsingJql", - "Bash(gh:*)" + "Bash(gh:*)", + "WebSearch" ], "deny": [ "Bash(git push*main*)", From ba9c086c0198002d00701b4a648449e3ed58c2f6 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Wed, 29 Jul 2026 11:35:43 +0200 Subject: [PATCH 2/2] Write the Chrome settings Google does not sync: vertical tabs, side panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chrome syncs bookmarks, extensions, passwords and most of the Settings page, but a handful of window-chrome toggles are per-installation and never leave the machine. Vertical tabs is the one that hurts: every new Mac starts with the horizontal strip again, and the fix is a menu item nobody remembers the name of. So `make chrome` writes them, the same way `make macos` writes the System Settings panes. Not a stow package: Chrome keeps them in `Default/Preferences`, one JSON blob that also holds site permissions, engagement scores, an upload seed and the last window rectangle. Symlinking that into git would track the noise and publish the rest, so only the four keys named in the script are patched and the rest of the file is left exactly as the machine has it. Two things the script refuses to do quietly. It will not write while Chrome is running, because Chrome holds the file in memory and rewrites it on exit — the change would disappear hours later, which is the worst kind of no-op. And it writes through a temp file and a rename, so an interrupted run leaves the old Preferences intact rather than half a JSON document. python3 rather than jq: it is on macOS and on both runners, and jq is not in the Brewfile. --- Makefile | 8 ++++ README.md | 7 ++++ bin/chrome/prefs.sh | 85 ++++++++++++++++++++++++++++++++++++++++ bin/chrome/prefs.test.sh | 69 ++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+) create mode 100755 bin/chrome/prefs.sh create mode 100755 bin/chrome/prefs.test.sh diff --git a/Makefile b/Makefile index d2d07bf..431aadd 100644 --- a/Makefile +++ b/Makefile @@ -167,6 +167,14 @@ jetbrains: ## set the JVM options this repo owns in every JetBrains config dir @echo @echo "plugins: open $(CURDIR) in the IDE and accept the 'required plugins' prompt" +# +# Google Chrome — the toggles Google does not sync, see bin/chrome/prefs.sh +# + +.PHONY: chrome +chrome: ## apply the Chrome settings that do not sync (vertical tabs, side panel) + ./bin/chrome/prefs.sh + # # macOS System Settings — the panes stow cannot reach, see bin/macos/defaults.sh # diff --git a/README.md b/README.md index 524d7fd..2323e8f 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,15 @@ make git # hook in .gitconfig, set email + commit signing make macos # macOS only: menu bar, Dock, Finder, trackpad, formats make macos-touchid # macOS only: authenticate sudo with Touch ID (asks for root) make jetbrains # set the IDE heap; then open this repo in the IDE to get the plugins +make chrome # Chrome's non-syncing toggles — quit Chrome first ``` +`make chrome` covers the handful of Chrome settings that stay on the machine +instead of following the Google account: vertical tabs and which side the side +panel opens on. Chrome rewrites `Default/Preferences` when it exits, so the +target refuses to run while it is open — everything else in that file (site +permissions, history, the window rectangle) is left untouched. + Opening this repo in a JetBrains IDE is the last step: it offers to install every plugin in `.idea/externalDependencies.xml` in one click, and plugins are installed per IDE rather than per project, so that one prompt covers every project on the diff --git a/bin/chrome/prefs.sh b/bin/chrome/prefs.sh new file mode 100755 index 0000000..3e77cc0 --- /dev/null +++ b/bin/chrome/prefs.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Apply the Chrome settings that Google does not sync. +# +# Chrome syncs bookmarks, extensions, passwords and most of the Settings page, +# but a handful of window-chrome toggles are per-installation and never leave +# the machine — vertical tabs is the obvious one. Those are what this writes. +# +# A script and not a stow package: Chrome keeps them in `Default/Preferences`, +# one JSON blob that also holds site permissions, engagement scores, an upload +# seed and a window rectangle. Symlinking that into git would track the noise +# and leak the rest, so only the keys listed below are written and everything +# else is left as the machine has it. Same reasoning as bin/macos/defaults.sh. +# +# To add a setting: copy Preferences aside, flip the toggle in Chrome, quit it, +# then diff — Chrome only flushes the file on exit: +# cp "$CHROME_PREFS" /tmp/before # …flip it, quit Chrome… +# python3 -m json.tool "$CHROME_PREFS" | diff <(python3 -m json.tool /tmp/before) - +# and paste the dotted key here with its JSON value. +# +# usage: prefs.sh [--dry-run] +set -eu + +[ "${1:-}" != "--dry-run" ] || DRY_RUN=1 +: "${DRY_RUN:=}" + +# One `dotted.key ` per line. Split on the first space, so a string +# value has to be JSON without spaces ("en-US", not "en US") — none needs them. +prefs=' +vertical_tabs.enabled true +vertical_tabs.collapsed_state false +vertical_tabs.uncollapsed_width 240 +side_panel.is_right_aligned false +' + +# ponytail: the Default profile only. A second profile is a second $CHROME_PREFS +# run, and this repo has never had one. +case "$(uname -s)" in +Darwin) default_prefs="$HOME/Library/Application Support/Google/Chrome/Default/Preferences" ;; +*) default_prefs="$HOME/.config/google-chrome/Default/Preferences" ;; +esac +: "${CHROME_PREFS:=$default_prefs}" + +if [ -n "$DRY_RUN" ]; then + echo "$prefs" | grep -v '^$' | sed "s|^|set |" + echo "file $CHROME_PREFS" + exit 0 +fi + +[ -f "$CHROME_PREFS" ] || { echo "no Chrome profile at $CHROME_PREFS"; exit 1; } + +# Chrome holds the whole file in memory and rewrites it on exit, so a write made +# while it runs is silently discarded a few hours later — the worst kind of +# no-op. Refuse instead. +if pgrep -x "Google Chrome" >/dev/null 2>&1 || pgrep -x chrome >/dev/null 2>&1; then + echo "quit Chrome first - it overwrites $CHROME_PREFS on exit"; exit 1 +fi + +# python3 and not jq: it ships with macOS and with every runner, and jq is not +# in the Brewfile. Written to a temp file and renamed, so an interrupted run +# leaves the old Preferences intact rather than half a JSON document. +PREFS="$prefs" python3 - "$CHROME_PREFS" <<'PY' +import json, os, sys, tempfile + +path = sys.argv[1] +with open(path, encoding="utf-8") as f: + prefs = json.load(f) + +for line in os.environ["PREFS"].split("\n"): + if not line.strip(): + continue + key, value = line.split(" ", 1) + *parents, leaf = key.split(".") + node = prefs + for parent in parents: + node = node.setdefault(parent, {}) + node[leaf] = json.loads(value) + print("set %s = %s" % (key, value)) + +fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path) or ".") +with os.fdopen(fd, "w", encoding="utf-8") as f: + json.dump(prefs, f, separators=(",", ":")) # compact, the way Chrome writes it +os.replace(tmp, path) +PY + +echo "applied - start Chrome" diff --git a/bin/chrome/prefs.test.sh b/bin/chrome/prefs.test.sh new file mode 100755 index 0000000..af88b4c --- /dev/null +++ b/bin/chrome/prefs.test.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Self-check for ./prefs.sh. Runs it against a throwaway Preferences file via +# $CHROME_PREFS, with a `pgrep` stub on PATH, so it never touches the real +# Chrome profile and gives the same answer on a runner with no Chrome at all. +set -eu + +script="$(cd "$(dirname "$0")" && pwd)/prefs.sh" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +fails=0 + +check() { # check + if [ "$2" = "$3" ]; then + echo "ok $1" + else + echo "FAIL $1"; echo " expected: $2"; echo " actual: $3"; fails=$((fails + 1)) + fi +} + +# A stand-in profile: one key this repo writes into, and one it must not touch. +# The nested "keep" is the point — Chrome's Preferences is one blob, and a patch +# that rewrites a whole subtree instead of a leaf loses site permissions. +printf '{"vertical_tabs":{"enabled":false,"enabled_first_time":true},"profile":{"keep":"me"}}' > "$tmp/Preferences" + +# pgrep says "nothing running", so the guard lets the write through. +printf '#!/bin/sh\nexit 1\n' > "$tmp/pgrep" +chmod +x "$tmp/pgrep" + +out=$(PATH="$tmp:$PATH" CHROME_PREFS="$tmp/Preferences" "$script") +read_key() { python3 -c 'import json,sys;d=json.load(open(sys.argv[1])) +for k in sys.argv[2].split("."): d=d[k] +print(json.dumps(d))' "$tmp/Preferences" "$1"; } + +check "the file is still valid JSON" '0' "$(python3 -m json.tool "$tmp/Preferences" >/dev/null 2>&1; echo $?)" +check "vertical tabs are on" 'true' "$(read_key vertical_tabs.enabled)" +check "the sibling key survives" 'true' "$(read_key vertical_tabs.enabled_first_time)" +check "an unrelated subtree survives" '"me"' "$(read_key profile.keep)" +check "a missing parent is created" 'false' "$(read_key side_panel.is_right_aligned)" +check "it reports what it wrote" '4' "$(grep -c '^set ' <<<"$out")" + +# Running it twice must not drift — this is what `make chrome` does on every +# machine, every time. +before=$(cat "$tmp/Preferences") +PATH="$tmp:$PATH" CHROME_PREFS="$tmp/Preferences" "$script" >/dev/null +check "idempotent" "$before" "$(cat "$tmp/Preferences")" + +# The guard: with Chrome running the write is discarded on its exit, so it must +# refuse rather than pretend. +printf '#!/bin/sh\nexit 0\n' > "$tmp/pgrep" +set +e +out=$(PATH="$tmp:$PATH" CHROME_PREFS="$tmp/Preferences" "$script" 2>&1); status=$? +set -e +check "refuses while Chrome runs" '1' "$status" +check "and says why" '1' "$(grep -c 'quit Chrome' <<<"$out")" + +# --dry-run must not need a profile, or reach one. +out=$(CHROME_PREFS="$tmp/absent" "$script" --dry-run) +check "dry run writes nothing" 'no' "$([ -f "$tmp/absent" ] && echo yes || echo no)" +check "dry run lists every key" '4' "$(grep -c '^set ' <<<"$out")" + +# Same shape the script parses: key, one space, JSON value. A pasted line with a +# space in the value would set the key to garbage without this. +keys=$(sed -n 's/^set \([^ ]*\) .*/\1/p' <<<"$out") +check "no key listed twice" '' "$(sort <<<"$keys" | uniq -d)" +check "every value is one JSON token" '4' \ + "$(grep -c '^set [a-z_.]* [^ ]*$' <<<"$out")" + +[ "$fails" -eq 0 ] || { echo "$fails failed"; exit 1; } +echo "all passed"