Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ The one‑liner script will:
1. Download the `cursor.sh` script and save it as `cursor-installer` in `~/.local/bin/`
2. Download `lib.sh` to `~/.local/share/cursor-installer/lib.sh`
3. Make the script executable
4. Download and install the latest version of Cursor
4. Install a `cursor` shim at `~/.local/bin/cursor` (see [The `cursor` Shim](#the-cursor-shim))
5. Download and install the latest version of Cursor

**Note:** If you're installing via the piped bash method and don't have FUSE2 installed, the script will warn you but continue. You'll need to either:

Expand Down Expand Up @@ -126,6 +127,8 @@ The uninstall script will:
3. Remove the Cursor AppImage
4. Ask if you want to remove the Cursor configuration files

**Note:** The `cursor` shim at `~/.local/bin/cursor` is not removed by the uninstall script. See [Removing the Shim](#removing-the-shim) for manual cleanup.

## Usage

Note: The installer CLI is `cursor-installer` to avoid conflicts with Cursor's official `cursor` CLI.
Expand Down Expand Up @@ -196,6 +199,69 @@ cursor-installer --update stable

**Note:** The extracted installation is stored in `~/.local/share/cursor/`.

## The `cursor` Shim

### Why

Cursor now ships an official `cursor` CLI, but this project predates it. The installer CLI is named `cursor-installer` to avoid colliding with the official binary. On systems where Cursor was installed through this project alone, there may be no `cursor` command on PATH.

The shim bridges that gap. It installs a lightweight script at `~/.local/bin/cursor` that transparently forwards to the real Cursor CLI when present, and falls back to `cursor-installer` when it isn't.

### What It Does

When you type `cursor`, the shim (`~/.local/bin/cursor`) follows a short resolution chain:

1. **Real Cursor binary found in PATH?** -- Forward all arguments to it (e.g. Cursor's official `cursor` CLI).
2. **`cursor agent` subcommand?** -- Delegate to `~/.local/bin/agent` if it exists.
3. **`cursor-installer` found?** -- Delegate to the installer CLI so commands like `cursor --update` still work.
4. **Nothing found** -- Print a helpful error with install instructions.

The shim never hides a real Cursor binary; it only acts as a fallback.

### How It Works

Two scripts cooperate:

| File | Purpose |
| --- | --- |
| `shim.sh` | The shim itself, installed as `~/.local/bin/cursor`. |
| `scripts/ensure-shim.sh` | Idempotent installer/updater for the shim. |

**`ensure-shim.sh` safety guards:**

- If `~/.local/bin/cursor` does not exist, the shim is installed.
- If it exists and is already the current shim (byte-identical), nothing happens.
- If it exists and _is_ a shim (detected by shebang + marker string), it is updated.
- If it exists and is _not_ a shim (e.g. a real Cursor binary, a symlink you created, etc.), **it is never overwritten**.

### When It Runs

The shim is synced automatically during normal installer operations:

- **`install.sh`** -- Copies `shim.sh` and `ensure-shim.sh` into `~/.local/share/cursor-installer/`, then runs `ensure-shim.sh`.
- **`cursor-installer --update`** -- Re-downloads the latest shim assets from GitHub, then re-runs `ensure-shim.sh`.
- **`cursor-installer` (install paths)** -- Runs `ensure-shim.sh` before each install to keep the shim current.

### File Locations

| Path | Description |
| --- | --- |
| `~/.local/bin/cursor` | The shim (what you invoke). |
| `~/.local/share/cursor-installer/shim.sh` | Cached copy of the shim source. |
| `~/.local/share/cursor-installer/ensure-shim.sh` | Cached copy of the installer helper. |

### Removing the Shim

The uninstall script does not currently remove the shim. To remove it manually:

```bash
rm ~/.local/bin/cursor
rm -f ~/.local/share/cursor-installer/shim.sh
rm -f ~/.local/share/cursor-installer/ensure-shim.sh
```

If you only want to disable the shim without uninstalling the rest of the project, removing `~/.local/bin/cursor` is sufficient.

## Note

If you encounter a warning that `~/.local/bin` is not in your PATH, you can add it by running:
Expand Down
5 changes: 4 additions & 1 deletion cursor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function get_extracted_root() {
fi
done
return 1
}
}

function get_extraction_dir() {
# Prefer ~/.local/share/cursor for extracted installations
Expand Down Expand Up @@ -289,6 +289,7 @@ EOF
}

function install_cursor_extracted() {
run_ensure_shim
local install_dir="$1"
local release_track=${2:-stable}
local temp_file
Expand Down Expand Up @@ -438,6 +439,7 @@ function install_cursor_extracted() {
}

function install_cursor() {
run_ensure_shim
local install_dir="$1"
local release_track=${2:-stable} # Default to stable if not specified

Expand Down Expand Up @@ -663,6 +665,7 @@ EOF

function update_cursor() {
log_step "Updating Cursor..."
refresh_shim_assets
local current_appimage
current_appimage=$(find_cursor_appimage || true)
local install_dir
Expand Down
15 changes: 8 additions & 7 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,15 @@ done
# Determine whether to use local cursor.sh or download from GitHub
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
LOCAL_CURSOR_SH="$SCRIPT_DIR/cursor.sh"

# Repo URL parameters (override via env for release workflows)
LIB_DIR="$HOME/.local/share/cursor-installer"
LIB_PATH="$SCRIPT_DIR/lib.sh"
SHARED_LIB="$LIB_DIR/lib.sh"
REPO_OWNER=${REPO_OWNER:-watzon}
REPO_BRANCH=${REPO_BRANCH:-main}
REPO_NAME=${REPO_NAME:-cursor-linux-installer}
BASE_RAW_URL="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${REPO_BRANCH}"
CURSOR_SCRIPT_URL="$BASE_RAW_URL/cursor.sh"
LIB_DIR="$HOME/.local/share/cursor-installer"
LIB_PATH="$SCRIPT_DIR/lib.sh"
SHARED_LIB="$LIB_DIR/lib.sh"
LIB_URL="$BASE_RAW_URL/lib.sh"
LIB_URL="${BASE_RAW_URL}/lib.sh"
CURSOR_SCRIPT_URL="${BASE_RAW_URL}/cursor.sh"

# Source shared helpers (local repo, installed lib, or download)
if [ -f "$LIB_PATH" ]; then
Expand Down Expand Up @@ -96,6 +94,9 @@ chmod +x "$CLI_PATH"

log_ok "Cursor installer script has been placed in $CLI_PATH"

log_step "Ensuring cursor shim..."
LOCAL_SHIM_PATH="$SCRIPT_DIR/shim.sh" LOCAL_SHIM_HELPER_PATH="$SCRIPT_DIR/scripts/ensure-shim.sh" sync_shim_assets && run_ensure_shim || log_warn "Shim update skipped or failed; continuing."

# Check if ~/.local/bin is in PATH
if [[ ":$PATH:" != *":$LOCAL_BIN:"* ]]; then
log_warn "$LOCAL_BIN is not in your PATH."
Expand Down
56 changes: 56 additions & 0 deletions lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,59 @@ function find_cursor_appimage() {
done
return 1
}

# --- Shim (cursor in PATH): canonical paths and helpers ---
# Requires LIB_DIR to be set by caller before sourcing lib.
REPO_OWNER="${REPO_OWNER:-watzon}"
REPO_BRANCH="${REPO_BRANCH:-main}"
REPO_NAME="${REPO_NAME:-cursor-linux-installer}"
BASE_RAW_URL="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${REPO_BRANCH}"
SHIM_TARGET="${SHIM_TARGET:-$HOME/.local/bin/cursor}"
SHARED_SHIM="${LIB_DIR}/shim.sh"
SHIM_HELPER="${LIB_DIR}/ensure-shim.sh"
SHIM_URL="${BASE_RAW_URL}/shim.sh"
SHIM_HELPER_URL="${BASE_RAW_URL}/scripts/ensure-shim.sh"
LIB_URL="${BASE_RAW_URL}/lib.sh"
CURSOR_SCRIPT_URL="${BASE_RAW_URL}/cursor.sh"

# Sync shim.sh and ensure-shim.sh into LIB_DIR (local copy or download).
# Set LOCAL_SHIM_PATH and/or LOCAL_SHIM_HELPER_PATH to prefer repo files.
function sync_shim_assets() {
mkdir -p "$LIB_DIR"
if [ -n "${LOCAL_SHIM_PATH:-}" ] && [ -f "$LOCAL_SHIM_PATH" ]; then
cp "$LOCAL_SHIM_PATH" "$SHARED_SHIM"
elif [ ! -f "$SHARED_SHIM" ]; then
curl -fsSL "$SHIM_URL" -o "$SHARED_SHIM" || { log_warn "Failed to download shim.sh"; return 1; }
fi
if [ -n "${LOCAL_SHIM_HELPER_PATH:-}" ] && [ -f "$LOCAL_SHIM_HELPER_PATH" ]; then
cp "$LOCAL_SHIM_HELPER_PATH" "$SHIM_HELPER"
elif [ ! -f "$SHIM_HELPER" ]; then
curl -fsSL "$SHIM_HELPER_URL" -o "$SHIM_HELPER" || { log_warn "Failed to download ensure-shim.sh"; return 1; }
fi
chmod +x "$SHIM_HELPER" "$SHARED_SHIM" 2>/dev/null || true
return 0
}

# Refresh shim assets from GitHub (used on cursor-installer --update).
function refresh_shim_assets() {
log_step "Refreshing cursor shim assets..."
mkdir -p "$LIB_DIR"
if ! curl -fsSL "$SHIM_URL" -o "$SHARED_SHIM"; then
log_warn "Failed to download shim.sh; continuing."
return 0
fi
if ! curl -fsSL "$SHIM_HELPER_URL" -o "$SHIM_HELPER"; then
log_warn "Failed to download ensure-shim.sh; continuing."
return 0
fi
chmod +x "$SHIM_HELPER" "$SHARED_SHIM" 2>/dev/null || true
}

# Run ensure-shim.sh with canonical SOURCE_SHIM and TARGET_SHIM.
function run_ensure_shim() {
if [ ! -x "$SHIM_HELPER" ] && [ ! -f "$SHIM_HELPER" ]; then
log_info "Shim helper not found; skipping shim update."
return 0
fi
SOURCE_SHIM="$SHARED_SHIM" TARGET_SHIM="$SHIM_TARGET" "$SHIM_HELPER" || { log_warn "Shim update failed; continuing."; return 0; }
}
73 changes: 73 additions & 0 deletions scripts/ensure-shim.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/bin/sh
# Install or update ~/.local/bin/cursor shim. Skips if existing file is already our shim.
set -eu

TARGET_SHIM="${TARGET_SHIM:-$HOME/.local/bin/cursor}"
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
LIB_DIR="${HOME}/.local/share/cursor-installer"

SOURCE_SHIM="${SOURCE_SHIM:-}"
if [ -z "$SOURCE_SHIM" ]; then
if [ -f "$LIB_DIR/shim.sh" ]; then
SOURCE_SHIM="$LIB_DIR/shim.sh"
elif [ -f "$SCRIPT_DIR/shim.sh" ]; then
SOURCE_SHIM="$SCRIPT_DIR/shim.sh"
elif [ -f "$SCRIPT_DIR/../shim.sh" ]; then
SOURCE_SHIM="$SCRIPT_DIR/../shim.sh"
fi
fi

if [ -z "$SOURCE_SHIM" ] || [ ! -f "$SOURCE_SHIM" ]; then
echo "Error: shim.sh source not found." >&2
exit 1
fi

is_shim() {
file="$1"
[ -f "$file" ] || return 1
first_line=$(head -n 1 "$file" 2>/dev/null || true)
case "$first_line" in
"#!/bin/sh"|\
"#!/usr/bin/env sh"|\
"#!/bin/bash"|\
"#!/usr/bin/env bash")
;;
*)
return 1
;;
esac
if grep -q "Find cursor executable in PATH" "$file" 2>/dev/null; then
return 0
fi
if grep -q "cursor-installer" "$file" 2>/dev/null; then
return 0
Comment on lines +42 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict shim detection before replacing existing cursor

The non-shim protection is too broad: any executable shell script containing the text cursor-installer is treated as an existing shim and eligible for overwrite. That means user-managed wrappers at ~/.local/bin/cursor can be silently replaced even when they are not this project's shim, which contradicts the safety guarantee in this change.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

@ZanzyTHEbar ZanzyTHEbar Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, also, seems like an edge-case that does not practically exist. When would a user ever have another cursor-installer on PATH?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair honestly

fi
return 1
}

is_current_shim() {
is_shim "$TARGET_SHIM" || return 1
cmp -s "$SOURCE_SHIM" "$TARGET_SHIM"
}

if is_current_shim; then
echo "Cursor shim already installed; skipping."
exit 0
fi

if [ ! -e "$TARGET_SHIM" ]; then
mkdir -p "$(dirname "$TARGET_SHIM")"
cp "$SOURCE_SHIM" "$TARGET_SHIM"
chmod +x "$TARGET_SHIM"
echo "Installed cursor shim at $TARGET_SHIM"
exit 0
fi

if ! is_shim "$TARGET_SHIM"; then
echo "Skipping shim update; existing cursor is not a shim: $TARGET_SHIM"
exit 0
fi

cp "$SOURCE_SHIM" "$TARGET_SHIM"
chmod +x "$TARGET_SHIM"
echo "Updated cursor shim at $TARGET_SHIM"
46 changes: 46 additions & 0 deletions shim.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/sh
set -eu

# Find cursor executable in PATH, excluding the current shim
find_cursor() {
old_IFS="$IFS"
IFS=:
for dir in $PATH; do
[ -n "$dir" ] || continue
cursor_path="$dir/cursor"
if [ "$cursor_path" != "$HOME/.local/bin/cursor" ] && [ -x "$cursor_path" ]; then
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude the active shim path when resolving cursor

The PATH probe only excludes the literal string $HOME/.local/bin/cursor, so if the shim is invoked through any other path to the same file (for example a symlinked PATH entry or a non-default TARGET_SHIM location), find_cursor can rediscover the shim itself and exec back into it indefinitely. In that setup, cursor hangs in a self-recursive loop instead of reaching cursor-installer or a real Cursor binary.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would the shim be invoked from anywhere else? This seems like a non-existent edgecase.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just Codex doing Codex things haha. It does like to be thorough.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! So after looking at this, I took some time to stress test my local setup and I WAS able to hit this edge-case.

I will provide a fix for it.

How I did it:

  • machine where cursor installer was used as primary mechanism
  • install cursor cli
  • run cursor installer AGAIN to overwrite cursor cli bash script entrypoint with shim

This caused the shim to re-discover itself as the shim only used raw string comparison.

I was then faced with a terminal "hang" as the shim re-executed itself.

IFS="$old_IFS"
echo "$cursor_path"
return 0
fi
done
IFS="$old_IFS"
return 1
}

OTHER_CURSOR=$(find_cursor || true)
CURSOR_INSTALLER=$(command -v cursor-installer 2>/dev/null || true)
AGENT_BIN="$HOME/.local/bin/agent"

if [ -n "${OTHER_CURSOR:-}" ]; then
exec "$OTHER_CURSOR" "$@"
fi

first_arg="${1:-}"

if [ "$first_arg" = "agent" ]; then
if [ -x "$AGENT_BIN" ]; then
exec "$AGENT_BIN" "$@"
fi
echo "Error: Cursor agent not found at $AGENT_BIN" 1>&2
exit 1
fi

if [ -n "${CURSOR_INSTALLER:-}" ]; then
exec "$CURSOR_INSTALLER" "$@"
fi

echo "Error: No Cursor IDE installation found." 1>&2
echo "Install/update with: cursor-installer --update [stable|latest]" 1>&2
echo "Or, install Cursor at https://cursor.com/download" 1>&2
exit 1