Skip to content

Instantly share code, notes, and snippets.

@cmhobbs
Created August 4, 2026 00:20
Show Gist options
  • Select an option

  • Save cmhobbs/4831054d64020941cbfbacf5f48c1dbf to your computer and use it in GitHub Desktop.

Select an option

Save cmhobbs/4831054d64020941cbfbacf5f48c1dbf to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# new-vivaldi-profile.sh <client-name>
# Creates a new Vivaldi profile from the profile named "Template".
# Run with Vivaldi closed.
set -euo pipefail
CLIENT_NAME="${1:-}"
if [[ -z "$CLIENT_NAME" ]]; then
echo "Usage: $0 \"Client Name\""
exit 1
fi
VIVALDI_DIR="$HOME/.var/app/com.vivaldi.Vivaldi/config/vivaldi"
LOCAL_STATE="$VIVALDI_DIR/Local State"
if [[ ! -f "$LOCAL_STATE" ]]; then
echo "ERROR: Vivaldi config not found. Is Vivaldi installed as a Flatpak?"
exit 1
fi
# Vivaldi keeps Local State in memory and will overwrite our edits on its
# next flush. Refuse to run if it's up.
if flatpak ps --columns=application 2>/dev/null | grep -qx 'com.vivaldi.Vivaldi'; then
echo "ERROR: Vivaldi is running. Close it (check for background instances) and re-run."
exit 1
fi
# Find Template profile directory name
TEMPLATE_DIR=$(CLIENT_NAME="$CLIENT_NAME" LOCAL_STATE="$LOCAL_STATE" python3 -c "
import json, os, sys
with open(os.environ['LOCAL_STATE']) as f:
state = json.load(f)
for dir_name, info in state.get('profile', {}).get('info_cache', {}).items():
if info.get('name', '').lower() == 'template':
print(dir_name)
sys.exit(0)
")
if [[ -z "$TEMPLATE_DIR" ]]; then
echo "ERROR: No profile named 'Template' found. Create one in Vivaldi first."
exit 1
fi
TEMPLATE_PATH="$VIVALDI_DIR/$TEMPLATE_DIR"
if [[ ! -d "$TEMPLATE_PATH" ]]; then
echo "ERROR: Template directory not found: $TEMPLATE_PATH"
exit 1
fi
# Find next available Profile N
N=1
while [[ -d "$VIVALDI_DIR/Profile $N" ]]; do
N=$((N + 1))
done
NEW_DIR="Profile $N"
NEW_PATH="$VIVALDI_DIR/$NEW_DIR"
echo "Copying template to $NEW_DIR..."
cp -r "$TEMPLATE_PATH" "$NEW_PATH"
echo "Clearing session and account data..."
# Individual files to remove
for f in \
"Cookies" "Cookies-journal" \
"Login Data" "Login Data-journal" \
"Login Data For Account" "Login Data For Account-journal" \
"Account Web Data" "Account Web Data-journal" \
"Affiliation Database" "Affiliation Database-journal" \
"History" "History-journal" \
"Web Data" "Web Data-journal" \
"Favicons" "Favicons-journal" \
"Top Sites" "Top Sites-journal" \
"Shortcuts" "Shortcuts-journal" \
"Network Action Predictor" "Network Action Predictor-journal" \
"Safe Browsing Cookies" "Safe Browsing Cookies-journal" \
"Trust Tokens" "Trust Tokens-journal" \
"heavy_ad_intervention_opt_out.db" "heavy_ad_intervention_opt_out.db-journal" \
"SCT Auditing Pending Reports" \
"BookmarkMergedSurfaceOrdering" \
"trusted_vault.pb" "DIPS" \
"LOCK" "LOG"; do
rm -f "$NEW_PATH/$f"
done
# Directories to remove
for d in \
"Sessions" "Session Storage" \
"GPUCache" "DawnGraphiteCache" "DawnWebGPUCache" \
"blob_storage" "Service Worker" \
"Storage" "Local Storage" "WebStorage" \
"Sync Data" "SyncedFiles" "Sync Extension Settings" \
"VivaldiThumbnails" "VivaldiDirectMatchIcons" \
"shared_proto_db" "SharedStorage" "Shared Dictionary" \
"Site Characteristics Database" "Feature Engagement Tracker" \
"GCM Store" "discount_infos_db" "discounts_db" \
"parcel_tracking_db" "commerce_subscription_db" \
"AutofillStrikeDatabase" "BudgetDatabase" \
"VideoDecodeStats" "chrome_cart_db"; do
rm -rf "$NEW_PATH/$d"
done
echo "Backing up Local State..."
BACKUP="$LOCAL_STATE.bak.$(date +%Y%m%d-%H%M%S)"
cp -p "$LOCAL_STATE" "$BACKUP"
echo "Registering profile..."
CLIENT_NAME="$CLIENT_NAME" TEMPLATE_DIR="$TEMPLATE_DIR" NEW_DIR="$NEW_DIR" LOCAL_STATE="$LOCAL_STATE" python3 -c "
import json, copy, os, tempfile
local_state = os.environ['LOCAL_STATE']
template_dir = os.environ['TEMPLATE_DIR']
new_dir = os.environ['NEW_DIR']
client_name = os.environ['CLIENT_NAME']
with open(local_state) as f:
state = json.load(f)
cache = state['profile']['info_cache']
new_info = copy.deepcopy(cache.get(template_dir, {}))
new_info['name'] = client_name
new_info['is_using_default_name'] = False
new_info['gaia_name'] = ''
new_info['gaia_given_name'] = ''
new_info['gaia_id'] = ''
new_info['user_name'] = ''
new_info['managed_user_id'] = ''
new_info['is_consented_primary_account'] = False
state['profile']['info_cache'][new_dir] = new_info
order = state.get('profile', {}).get('profiles_order')
if order is not None and new_dir not in order:
order.append(new_dir)
# Write to a sibling tempfile and atomically rename so a partial write
# (power loss, full disk, kill) can never leave Local State truncated.
fd, tmp = tempfile.mkstemp(
dir=os.path.dirname(local_state),
prefix='Local State.',
suffix='.tmp',
)
try:
with os.fdopen(fd, 'w') as f:
json.dump(state, f, indent=3)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, local_state)
except Exception:
try:
os.unlink(tmp)
except FileNotFoundError:
pass
raise
"
echo "Backup saved to: $BACKUP"
echo ""
echo "Done. Profile '$CLIENT_NAME' created as $NEW_DIR."
echo "Launch Vivaldi and select it from the profile switcher."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment