Dominic Böttger

← Back to blog

Published on February 11, 2026 by Dominic Böttger · 16 min read

If you’ve switched to Arch Linux but still need access to SharePoint document libraries from work, you’ve probably noticed that Microsoft doesn’t offer a native OneDrive client for Linux. On Windows, clicking the “Sync” button in SharePoint just works. On Linux, nothing happens.

In this guide, I’ll show you how to make that “Sync” button work on Arch Linux - complete with automatic drive detection, subfolder sync, Windows-style folder naming, and systemd background services. Click “Sync” in your browser, and the files appear in ~/SharePoint/.

What We’re Building

When you click “Sync” on a SharePoint library or folder in your browser, SharePoint fires an odopen:// protocol URL. We’ll set up:

  1. abraunegg/onedrive - the open-source OneDrive sync client for Linux
  2. A protocol handler (odopen-handler.sh) that catches odopen:// URLs from the browser
  3. XDG desktop integration so the system routes these URLs to our handler
  4. Systemd user services for persistent background sync

The end result: click “Sync” in SharePoint, a terminal opens for first-time setup, and then your files sync in the background - just like on Windows.

Prerequisites

Step 1: Install abraunegg/onedrive

Install the OneDrive client from the AUR:

yay -S onedrive-abraunegg

This builds the client from source (it’s written in D, so it will pull in dmd as a build dependency).

Verify the installation:

onedrive --version
# onedrive v2.5.10

Step 2: Authenticate with Microsoft

You need to authenticate once so the client can obtain a refresh token. If you don’t need your entire personal OneDrive synced locally, you can skip the full sync and just do a one-time authentication.

Run onedrive and immediately cancel after authenticating:

onedrive --sync --verbose

This will print a URL - open it in your browser and sign in with your Microsoft account. Once the authentication completes, press Ctrl+C to stop the sync. The refresh token is now saved at ~/.config/onedrive/refresh_token.

If you don’t want any personal OneDrive files locally, remove the default sync directory:

rm -rf ~/OneDrive

The default OneDrive service is disabled by default - it won’t sync in the background unless you explicitly enable it:

# Verify it's not running (it shouldn't be)
systemctl --user status onedrive

Step 3: Install the Protocol Handler Script

Save the following script to ~/.local/bin/odopen-handler.sh:

#!/bin/bash
# =============================================================================
# odopen-handler.sh
# Protocol handler for odopen:// URLs (Teams/SharePoint "Sync" button)
# Uses abraunegg/onedrive as sync backend
# =============================================================================

set -euo pipefail

LOGFILE="$HOME/.local/share/odopen-handler/handler.log"
BASE_SYNC_DIR="$HOME/SharePoint"
BASE_CONFIG_DIR="$HOME/.config/onedrive-sharepoint"

mkdir -p "$(dirname "$LOGFILE")"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOGFILE"
}

# ---------------------------------------------------------------------------
# URL parsing helpers
# ---------------------------------------------------------------------------
urldecode() {
    python3 -c "import sys, urllib.parse; print(urllib.parse.unquote(sys.argv[1]))" "$1"
}

parse_param() {
    local value
    value=$(echo "$1" | grep -oP "${2}=\K[^&]+" | head -1 || true)
    if [[ -n "$value" ]]; then
        urldecode "$value"
    fi
}

# ---------------------------------------------------------------------------
# Notification helper (works with most DEs)
# ---------------------------------------------------------------------------
notify() {
    if command -v notify-send &>/dev/null; then
        notify-send -i folder-sync "OneDrive Sync" "$1"
    fi
    log "$1"
}

# ---------------------------------------------------------------------------
# --list: Show all active syncs
# ---------------------------------------------------------------------------
cmd_list() {
    local found=0
    for config_dir in "$BASE_CONFIG_DIR"/*/; do
        [[ -d "$config_dir" ]] || continue
        local name
        name=$(basename "$config_dir")
        local service_name="onedrive-sharepoint-${name}"
        local sync_dir=""
        local drive_id=""
        local status="stopped"

        if [[ -f "$config_dir/config" ]]; then
            sync_dir=$(grep -oP 'sync_dir\s*=\s*"\K[^"]+' "$config_dir/config" || true)
            drive_id=$(grep -oP 'drive_id\s*=\s*"\K[^"]+' "$config_dir/config" || true)
        fi

        if systemctl --user is-active "$service_name" &>/dev/null; then
            status="running"
        elif systemctl --user is-enabled "$service_name" &>/dev/null; then
            status="enabled (stopped)"
        fi

        if [[ $found -eq 0 ]]; then
            echo "Active OneDrive SharePoint syncs:"
            echo ""
        fi
        found=$((found + 1))
        echo "  [$found] $name"
        echo "      Sync dir:  $sync_dir"
        echo "      Drive ID:  ${drive_id:-(not set)}"
        echo "      Service:   $service_name ($status)"
        echo ""
    done

    if [[ $found -eq 0 ]]; then
        echo "No SharePoint syncs configured."
    fi
}

# ---------------------------------------------------------------------------
# --unsync: Remove a sync configuration
# ---------------------------------------------------------------------------
cmd_unsync() {
    local target="${1:-}"

    # Collect available syncs
    local names=()
    for config_dir in "$BASE_CONFIG_DIR"/*/; do
        [[ -d "$config_dir" ]] || continue
        names+=("$(basename "$config_dir")")
    done

    if [[ ${#names[@]} -eq 0 ]]; then
        echo "No SharePoint syncs configured."
        exit 0
    fi

    # If no target given, show interactive picker
    if [[ -z "$target" ]]; then
        echo "Which sync do you want to remove?"
        echo ""
        for i in "${!names[@]}"; do
            local n="${names[$i]}"
            local sd=""
            if [[ -f "$BASE_CONFIG_DIR/$n/config" ]]; then
                sd=$(grep -oP 'sync_dir\s*=\s*"\K[^"]+' "$BASE_CONFIG_DIR/$n/config" || true)
            fi
            local svc_status="stopped"
            if systemctl --user is-active "onedrive-sharepoint-${n}" &>/dev/null; then
                svc_status="running"
            fi
            echo "  [$((i + 1))] $n  ($svc_status)"
            [[ -n "$sd" ]] && echo "      $sd"
        done
        echo ""
        read -rp "Enter number (or 'q' to quit): " choice
        [[ "$choice" == "q" || -z "$choice" ]] && exit 0
        if ! [[ "$choice" =~ ^[0-9]+$ ]] || [[ "$choice" -lt 1 ]] || [[ "$choice" -gt ${#names[@]} ]]; then
            echo "Invalid selection."
            exit 1
        fi
        target="${names[$((choice - 1))]}"
    fi

    # Resolve target: allow passing display name, safe name, or sync dir name
    local safe_target=""
    for n in "${names[@]}"; do
        local sd=""
        if [[ -f "$BASE_CONFIG_DIR/$n/config" ]]; then
            sd=$(grep -oP 'sync_dir\s*=\s*"\K[^"]+' "$BASE_CONFIG_DIR/$n/config" || true)
        fi
        # Match against: safe name, sync dir basename, or sync dir full path
        if [[ "$n" == "$target" ]] || \
           [[ "$(basename "$sd")" == "$target" ]] || \
           [[ "$sd" == "$target" ]]; then
            safe_target="$n"
            break
        fi
    done
    if [[ -z "$safe_target" ]]; then
        echo "Error: No sync found for '$target'"
        echo "Use --list to see available syncs."
        exit 1
    fi

    local service_name="onedrive-sharepoint-${safe_target}"
    local sync_dir=""
    if [[ -f "$BASE_CONFIG_DIR/$safe_target/config" ]]; then
        sync_dir=$(grep -oP 'sync_dir\s*=\s*"\K[^"]+' "$BASE_CONFIG_DIR/$safe_target/config" || true)
    fi

    echo ""
    echo "Removing sync: $safe_target"
    echo "  Config:   $BASE_CONFIG_DIR/$safe_target"
    echo "  Sync dir: $sync_dir"
    echo "  Service:  $service_name"
    echo ""
    read -rp "Delete local files too? [y/N]: " delete_files

    # Stop and disable service
    systemctl --user stop "$service_name" 2>/dev/null || true
    systemctl --user disable "$service_name" 2>/dev/null || true
    rm -f "$HOME/.config/systemd/user/${service_name}.service"
    systemctl --user daemon-reload

    # Remove config
    rm -rf "$BASE_CONFIG_DIR/$safe_target"

    # Remove local files if requested
    if [[ "$delete_files" =~ ^[yY]$ ]] && [[ -n "$sync_dir" ]]; then
        rm -rf "$sync_dir"
        echo "Removed local files: $sync_dir"
    fi

    echo "Sync '$safe_target' removed."
    log "Unsynced: $safe_target"
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

case "${1:-}" in
    --list)
        cmd_list
        exit 0
        ;;
    --unsync)
        cmd_unsync "${2:-}"
        exit 0
        ;;
    --help|-h)
        echo "Usage:"
        echo "  odopen-handler.sh <odopen://...>   Handle SharePoint sync URL"
        echo "  odopen-handler.sh --list           List all active syncs"
        echo "  odopen-handler.sh --unsync [name]  Remove a sync (interactive if no name)"
        echo "  odopen-handler.sh --help           Show this help"
        exit 0
        ;;
esac

URI="${1:-}"

if [[ -z "$URI" ]]; then
    log "ERROR: No URI provided."
    notify "Error: No odopen:// URL provided."
    exit 1
fi

log "Received URI: $URI"

# --- Parse parameters from odopen:// URL ---
SITE_URL=$(parse_param "$URI" "webUrl")
FOLDER_URL=$(parse_param "$URI" "folderUrl")
FOLDER_NAME=$(parse_param "$URI" "folderName")
LIST_TITLE=$(parse_param "$URI" "listTitle")
WEB_TITLE=$(parse_param "$URI" "webTitle")
SITE_ID=$(parse_param "$URI" "siteId")
LIST_ID=$(parse_param "$URI" "listId")
WEB_ID=$(parse_param "$URI" "webId")

# Strip curly braces (SharePoint sometimes returns IDs with {})
SITE_ID="${SITE_ID//[\{\}]/}"
LIST_ID="${LIST_ID//[\{\}]/}"
WEB_ID="${WEB_ID//[\{\}]/}"

# --- Derive the relative folder path within the library ---
RELATIVE_FOLDER=""
if [[ -n "$FOLDER_URL" ]] && [[ -n "$SITE_URL" ]]; then
    LIB_AND_FOLDER="${FOLDER_URL#"$SITE_URL"}"
    LIB_AND_FOLDER="${LIB_AND_FOLDER#/}"
    RELATIVE_FOLDER="${LIB_AND_FOLDER#*/}"
    if [[ "$RELATIVE_FOLDER" == "$LIB_AND_FOLDER" ]]; then
        RELATIVE_FOLDER=""
    fi
fi

log "Parsed: site=$WEB_TITLE | library=$LIST_TITLE | folder=$FOLDER_NAME | relative_path=$RELATIVE_FOLDER"
log "Site URL: $SITE_URL"
log "Site ID: $SITE_ID | Web ID: $WEB_ID | List ID: $LIST_ID"

# --- Derive unique names - Windows-style "Sitename - Folder" ---
if [[ -n "$FOLDER_NAME" ]] && [[ -n "$RELATIVE_FOLDER" ]]; then
    DISPLAY_NAME="${WEB_TITLE} - ${FOLDER_NAME}"
    SAFE_NAME=$(echo "${WEB_TITLE}_${FOLDER_NAME}" | tr ' ' '_' | tr -cd '[:alnum:]_-')
    SINGLE_DIR="$RELATIVE_FOLDER"
else
    DISPLAY_NAME="${WEB_TITLE} - ${LIST_TITLE}"
    SAFE_NAME=$(echo "${WEB_TITLE}_${LIST_TITLE}" | tr ' ' '_' | tr -cd '[:alnum:]_-')
    SINGLE_DIR=""
fi

CONFIG_DIR="${BASE_CONFIG_DIR}/${SAFE_NAME}"
SYNC_DIR="${BASE_SYNC_DIR}/${DISPLAY_NAME}"
SERVICE_NAME="onedrive-sharepoint-${SAFE_NAME}"

log "Config dir: $CONFIG_DIR"
log "Sync dir: $SYNC_DIR"
log "Single dir: $SINGLE_DIR"

# --- Check if abraunegg/onedrive is installed ---
if ! command -v onedrive &>/dev/null; then
    notify "Error: abraunegg/onedrive is not installed!\nInstall with: yay -S onedrive-abraunegg"
    log "ERROR: onedrive not found in PATH"
    exit 1
fi

# --- Check if this library is already configured ---
if [[ -d "$CONFIG_DIR" ]] && [[ -f "$CONFIG_DIR/config" ]]; then
    notify "Sync for '$DISPLAY_NAME' is already configured.\nSync directory: $SYNC_DIR"

    if systemctl --user is-active "$SERVICE_NAME" &>/dev/null; then
        log "Service $SERVICE_NAME is already running."
        notify "Sync is already running in the background."
    else
        log "Starting existing service $SERVICE_NAME..."
        systemctl --user start "$SERVICE_NAME" || true
        notify "Sync has been restarted."
    fi
    exit 0
fi

# --- Create new configuration ---
mkdir -p "$CONFIG_DIR"
mkdir -p "$SYNC_DIR"

log "Creating new onedrive config in $CONFIG_DIR"

# Create config without drive_id (will be determined in the first-run script)
cat > "$CONFIG_DIR/config" <<EOF
# OneDrive SharePoint Sync - $DISPLAY_NAME
# Auto-generated by odopen-handler.sh on $(date)
sync_dir = "$SYNC_DIR"
EOF

# Check if a default OneDrive profile exists (for token reuse)
DEFAULT_CONFIG="$HOME/.config/onedrive"

if [[ -f "$DEFAULT_CONFIG/refresh_token" ]]; then
    log "Found existing OneDrive config, copying refresh_token..."
    cp "$DEFAULT_CONFIG/refresh_token" "$CONFIG_DIR/refresh_token" 2>/dev/null || true
    notify "Configuration created for: $DISPLAY_NAME\n\nDetermining Drive ID..."
else
    log "No existing OneDrive config found. First-run will require authentication."
    notify "New configuration created.\n\nAuthentication and Drive ID lookup in terminal..."
fi

# --- Build single-directory flags for onedrive CLI ---
SINGLE_DIR_FLAG=""
if [[ -n "$SINGLE_DIR" ]]; then
    SINGLE_DIR_FLAG="--single-directory '$SINGLE_DIR'"
fi

# --- Create systemd user service ---
SERVICE_FILE="$HOME/.config/systemd/user/${SERVICE_NAME}.service"
mkdir -p "$(dirname "$SERVICE_FILE")"

EXEC_CMD="/usr/bin/onedrive --monitor --confdir=\"$CONFIG_DIR\""
if [[ -n "$SINGLE_DIR" ]]; then
    EXEC_CMD="$EXEC_CMD --single-directory '$SINGLE_DIR'"
fi

cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=OneDrive SharePoint Sync - $DISPLAY_NAME
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=$EXEC_CMD
Restart=on-failure
RestartSec=30

# Performance & Safety
Nice=10
IOSchedulingClass=idle

[Install]
WantedBy=default.target
EOF

systemctl --user daemon-reload
log "Created systemd service: $SERVICE_NAME"

# --- Open interactive terminal for first-run setup ---
# Write env file (avoids sed issues with special chars like & in site names)
FIRST_RUN_ENV=$(mktemp /tmp/odopen-firstrun-XXXXXX.env)
cat > "$FIRST_RUN_ENV" <<ENVEOF
CONFIG_DIR=$(printf '%q' "$CONFIG_DIR")
SERVICE_NAME=$(printf '%q' "$SERVICE_NAME")
WEB_TITLE=$(printf '%q' "$WEB_TITLE")
LIST_TITLE=$(printf '%q' "$LIST_TITLE")
DISPLAY_NAME=$(printf '%q' "$DISPLAY_NAME")
SYNC_DIR=$(printf '%q' "$SYNC_DIR")
SINGLE_DIR=$(printf '%q' "$SINGLE_DIR")
ENVEOF

FIRST_RUN_SCRIPT=$(mktemp /tmp/odopen-firstrun-XXXXXX.sh)
cat > "$FIRST_RUN_SCRIPT" <<'FIRSTRUN'
#!/bin/bash
# Source environment variables
ENV_FILE="${BASH_SOURCE[0]%.sh}.env"
if [[ -f "$ENV_FILE" ]]; then
    source "$ENV_FILE"
else
    echo "ERROR: Environment file not found: $ENV_FILE"
    read
    exit 1
fi

echo "======================================================="
echo " OneDrive SharePoint Sync - First-Run Setup"
echo "======================================================="
echo ""
echo " Team/Site:    $WEB_TITLE"
echo " Library:      $LIST_TITLE"
if [[ -n "$SINGLE_DIR" ]]; then
echo " Folder:       $SINGLE_DIR"
fi
echo " Sync dir:     $SYNC_DIR"
echo " Config:       $CONFIG_DIR"
echo ""

# --- Step 1: Determine the Drive ID for this SharePoint library ---
echo "-------------------------------------------------------"
echo " Step 1: Determining Drive ID for '$WEB_TITLE'..."
echo " (First use: browser window will open for login)"
echo "-------------------------------------------------------"
echo ""

DRIVE_OUTPUT=$(onedrive --get-sharepoint-drive-id "$WEB_TITLE" 2>&1)
echo "$DRIVE_OUTPUT"
echo ""

if echo "$DRIVE_OUTPUT" | grep -q "ERROR"; then
    echo "======================================================="
    echo " ERROR: SharePoint site '$WEB_TITLE' not found."
    echo " Check the site name and your permissions."
    echo "======================================================="
    echo ""
    echo "Press Enter to close..."
    read
    exit 1
fi

# Extract drive_id - try exact match first, then auto-select if only one result
DRIVE_ID=$(echo "$DRIVE_OUTPUT" | grep -B1 "Library Name: *$LIST_TITLE" | grep -oP 'drive_id:\s+\K\S+' || true)

if [[ -z "$DRIVE_ID" ]]; then
    DRIVE_ID_COUNT=$(echo "$DRIVE_OUTPUT" | grep -c 'drive_id:' || true)
    if [[ "$DRIVE_ID_COUNT" -eq 1 ]]; then
        DRIVE_ID=$(echo "$DRIVE_OUTPUT" | grep -oP 'drive_id:\s+\K\S+')
        MATCHED_LIB=$(echo "$DRIVE_OUTPUT" | grep -oP 'Library Name:\s+\K.*')
        echo " Auto-selected: '$MATCHED_LIB' (only library found)"
    fi
fi

if [[ -z "$DRIVE_ID" ]]; then
    echo "-------------------------------------------------------"
    echo " Could not auto-match library '$LIST_TITLE'."
    echo " Available Drive IDs are shown above."
    echo ""
    echo " Please enter the drive_id (e.g. b!xxxxxx):"
    echo "-------------------------------------------------------"
    read -r DRIVE_ID
    if [[ -z "$DRIVE_ID" ]]; then
        echo "No Drive ID entered. Aborting."
        echo "Press Enter to close..."
        read
        exit 1
    fi
fi

echo ""
echo " Found Drive ID: $DRIVE_ID"
echo ""

# --- Step 2: Update config with drive_id ---
echo "-------------------------------------------------------"
echo " Step 2: Updating configuration..."
echo "-------------------------------------------------------"

cat > "$CONFIG_DIR/config" <<EOF2
# OneDrive SharePoint Sync - $DISPLAY_NAME
# Auto-generated by odopen-handler.sh
sync_dir = "$SYNC_DIR"
drive_id = "$DRIVE_ID"
EOF2

echo " Config written: $CONFIG_DIR/config"
echo ""

# --- Step 3: Initial synchronization ---
echo "-------------------------------------------------------"
echo " Step 3: Starting initial sync..."
if [[ -n "$SINGLE_DIR" ]]; then
echo " Folder: $SINGLE_DIR"
fi
echo "-------------------------------------------------------"
echo ""

SYNC_CMD=(onedrive --confdir="$CONFIG_DIR" --sync --verbose --resync --resync-auth)
if [[ -n "$SINGLE_DIR" ]]; then
    SYNC_CMD+=(--single-directory "$SINGLE_DIR")
fi

"${SYNC_CMD[@]}"

if [[ $? -eq 0 ]]; then
    echo ""
    echo "======================================================="
    echo " Initial sync completed successfully!"
    echo " Enabling background sync..."
    echo "======================================================="

    systemctl --user enable "$SERVICE_NAME"
    systemctl --user start "$SERVICE_NAME"

    echo ""
    echo " Service status:"
    systemctl --user status "$SERVICE_NAME" --no-pager || true
    echo ""
    echo " Your files are at: $SYNC_DIR"
    echo ""
    echo " Useful commands:"
    echo "   Status:    systemctl --user status $SERVICE_NAME"
    echo "   Stop:      systemctl --user stop $SERVICE_NAME"
    echo "   Logs:      journalctl --user -u $SERVICE_NAME -f"
    echo ""
else
    echo ""
    echo "======================================================="
    echo " ERROR during synchronization."
    echo " Check the output above for details."
    echo ""
    echo " Manual setup:"
    echo "   onedrive --confdir='$CONFIG_DIR' --sync --verbose"
    echo "======================================================="
fi

echo "Press Enter to close..."
read
FIRSTRUN

# Rename env file to match the script name so the script can find it
mv "$FIRST_RUN_ENV" "${FIRST_RUN_SCRIPT%.sh}.env"
chmod +x "$FIRST_RUN_SCRIPT"

# Open terminal - try available terminal emulators
open_terminal() {
    local cmd="$1"
    if command -v kitty &>/dev/null; then
        kitty --title "OneDrive Sync Setup" bash "$cmd" &
    elif command -v alacritty &>/dev/null; then
        alacritty --title "OneDrive Sync Setup" -e bash "$cmd" &
    elif command -v foot &>/dev/null; then
        foot --title "OneDrive Sync Setup" bash "$cmd" &
    elif command -v gnome-terminal &>/dev/null; then
        gnome-terminal --title "OneDrive Sync Setup" -- bash "$cmd" &
    elif command -v konsole &>/dev/null; then
        konsole -e bash "$cmd" &
    elif command -v xterm &>/dev/null; then
        xterm -title "OneDrive Sync Setup" -e bash "$cmd" &
    else
        log "ERROR: No terminal emulator found"
        notify "No terminal found!\nPlease run manually:\nbash $cmd"
        return 1
    fi
}

open_terminal "$FIRST_RUN_SCRIPT"
log "Opened terminal for first-run setup."

Make it executable:

chmod +x ~/.local/bin/odopen-handler.sh

Step 4: Register the odopen:// Protocol Handler

Create a desktop entry so the system knows how to handle odopen:// URLs:

cat > ~/.local/share/applications/odopen-handler.desktop <<EOF
[Desktop Entry]
Type=Application
Name=OneDrive Sync Handler
Comment=Protocol handler for odopen:// URLs (Teams/SharePoint Sync)
Exec=$HOME/.local/bin/odopen-handler.sh %u
MimeType=x-scheme-handler/odopen;
NoDisplay=true
Terminal=false
EOF

Register it with the system:

xdg-mime default odopen-handler.desktop x-scheme-handler/odopen
update-desktop-database ~/.local/share/applications/

Verify the registration:

xdg-mime query default x-scheme-handler/odopen
# Should output: odopen-handler.desktop

Step 5: Configure Your Browser

Your browser needs to allow odopen:// URLs to open external applications:

Usage

Syncing a SharePoint Library or Folder

  1. Navigate to a SharePoint document library or folder in your browser
  2. Click the “Sync” button
  3. Your browser will launch the protocol handler
  4. A terminal window opens showing the first-run setup:
    • Step 1: Queries the SharePoint API for the Drive ID
    • Step 2: Writes the configuration
    • Step 3: Performs the initial sync
  5. After the first sync completes, a systemd service starts monitoring for changes

Your files will appear in ~/SharePoint/ using Windows-style naming:

What you syncLocal folder
Entire library on “Project Alpha”~/SharePoint/Project Alpha - Documents/
Subfolder “Reports” on “Project Alpha”~/SharePoint/Project Alpha - Reports/

Listing Active Syncs

odopen-handler.sh --list

Output:

Active OneDrive SharePoint syncs:

  [1] ProjectAlpha_Reports
      Sync dir:  /home/user/SharePoint/Project Alpha - Reports
      Drive ID:  b!abc123...
      Service:   onedrive-sharepoint-ProjectAlpha_Reports (running)

Removing a Sync

Interactive mode (shows a picker):

odopen-handler.sh --unsync

Or specify the name directly:

odopen-handler.sh --unsync "Project Alpha - Reports"

The unsync command will:

  1. Stop and disable the systemd service
  2. Remove the configuration directory
  3. Optionally delete the local files (it will ask)

Useful systemd Commands

# Check sync status
systemctl --user status onedrive-sharepoint-<name>

# View live sync logs
journalctl --user -u onedrive-sharepoint-<name> -f

# Stop a sync temporarily
systemctl --user stop onedrive-sharepoint-<name>

# Restart a stopped sync
systemctl --user start onedrive-sharepoint-<name>

How It Works Under the Hood

When you click “Sync” in SharePoint, the browser opens a URL like:

odopen://sync?siteId={...}&webId={...}&webTitle=MyProject&listTitle=Documents
  &folderName=Reports&folderUrl=https://tenant.sharepoint.com/sites/MyProject/...

The handler script:

  1. Parses the URL to extract the site name, library title, folder name, and relative path
  2. Derives a unique config using the site and folder names, creating a separate onedrive configuration directory per sync
  3. Queries the SharePoint API via onedrive --get-sharepoint-drive-id to find the correct drive_id for the document library
  4. Handles localized library names - SharePoint sends “Documents” in the URL but the API might return “Dokumente” (German). The script auto-selects if there’s only one library
  5. Creates a systemd user service that runs onedrive --monitor with the specific --confdir and optional --single-directory flag
  6. Uses an env file instead of sed for variable passing to the first-run script, avoiding issues with special characters like & in site names

Gotchas and Tips

Don’t sync your entire personal OneDrive if you don’t need it. The default onedrive service is disabled. Only SharePoint syncs you explicitly set up via the “Sync” button will run.

Let the setup terminal finish. When the first-run terminal opens, let it complete all three steps before closing. The Drive ID gets written to the config during this process.

SharePoint uses localized names. The “Sync” button sends English names like “Documents”, but the API returns localized names like “Dokumente”. The script handles this automatically when there’s only one document library on the site.

Site names with special characters work. Names like “Jung Garten & Landschaft” are handled correctly through escaped environment variables.

The sync interval is 5 minutes by default. The onedrive client in monitor mode checks for changes every 300 seconds. You can adjust this with monitor_interval in the config file.

File indexers can cause issues. If you use Baloo or Tracker, consider excluding your ~/SharePoint/ directory from indexing to avoid accidental file modifications being synced upstream.

Written by Dominic Böttger

← Back to blog
  • Syncing SharePoint and OneDrive on Arch Linux with One Click

    Syncing SharePoint and OneDrive on Arch Linux with One Click

    Set up Microsoft SharePoint and OneDrive sync on Arch Linux using abraunegg/onedrive. Includes a protocol handler script that makes the "Sync" button in SharePoint work natively, with automatic drive detection and systemd background sync.

  • From Sequential to Parallel: Adding Agent Teams to Spec Kit

    From Sequential to Parallel: Adding Agent Teams to Spec Kit

    How we extended Spec Kit with a new /speckit.team-implement command that auto-detects parallel work streams from your task list and spawns specialized AI agent teams to implement features simultaneously -- complete source code, algorithm walkthrough, and usage guide.

  • Building a Private Claude Code Plugin Marketplace for Your Team

    Building a Private Claude Code Plugin Marketplace for Your Team

    Learn how to structure a private Claude Code plugin marketplace for your team, including repository layout, naming conventions, and how to register local or remote marketplaces.

  • Spec Kit + Ralph Loop: Solving AI Context Exhaustion in Large Features

    Spec Kit + Ralph Loop: Solving AI Context Exhaustion in Large Features

    How we combined Spec Kit's structured planning with Ralph Wiggum's fresh context methodology to build an AI-powered development loop that can implement features of any size without context pollution.

  • Mistral Releases Vibe CLI and Devstral 2: Open-Source AI Coding Goes Next Level

    Mistral Releases Vibe CLI and Devstral 2: Open-Source AI Coding Goes Next Level

    Mistral AI launches Vibe CLI and Devstral 2, bringing powerful open-source AI coding assistance to your terminal. Learn how to install and get started with these game-changing tools.

  • Auto Dark Mode on Linux Based on Real Sunrise and Sunset

    Auto Dark Mode on Linux Based on Real Sunrise and Sunset

    Set up automatic theme switching on Linux that follows the real sunrise and sunset times for your location, not just fixed schedules. Complete guide for Hyprland/Omarchy with systemd timers.