Skip to main content
rule

zsh — Cursor rule: what it does, how to install

This Cursor rule sets conventions for Zsh scripts and configuration. Use it when a project needs consistent structure, safer parameter handling, and ShellCheck checks.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/zsh.mdc
Open on GitHub

What zsh does

The rule asks scripts to use zsh as the interpreter and enable errreturn, nounset, and pipefail. It defines a main entry point, requires functions to appear before use, and sets formatting guidance such as 2-space indentation and an 80-character line limit.

It covers quoted variables, lower_snake_case names, native Zsh arrays, extended globbing, and parameter expansion for path handling. It also favors Zsh features for filtering and string changes instead of spawning commands such as dirname, basename, or grep.

ShellCheck is required as part of the editor and CI workflow. Without ShellCheck in those checks, the rule’s linting requirement is not covered.

What it helps with

  • enforce strict options in Zsh scripts
  • organize functions around a main entry point
  • check quoting and parameter expansion
  • handle paths with Zsh-native syntax
  • run ShellCheck in CI

Example prompts for the agent

  • Review this Zsh script against the rule and add strict options plus a main function.
  • Refactor .zshrc to follow the naming, quoting, and path-handling conventions.
  • Add ShellCheck validation for Zsh files in .github/workflows/ci.yml.

How to install zsh

Manually: put the files in place

.cursor/rules/zsh.mdc

Frontmatter: description, globs, alwaysApply

Step by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules

Questions about zsh

How do I install zsh in Cursor?

Create .cursor/rules/zsh.mdc and place the rule content in that file.

What does the zsh rule do?

It sets conventions for Zsh scripts and configuration, covering strict options, function layout, formatting, naming, arrays, globbing, and parameter expansion.

Does this rule require ShellCheck?

Yes. It requires ShellCheck to be integrated with the editor and CI, such as through ludeeus/action-shellcheck@v2 in .github/workflows/ci.yml.

Source text

zsh.mdc · CC0-1.0
---
description: This guide defines the definitive best practices for writing Zsh scripts and configuration files, ensuring consistency, robustness, and maintainability across our projects.
globs: **/*
---
# zsh Best Practices

Zsh is our default shell for interactive use and scripting. This guide outlines mandatory practices to ensure all Zsh code is robust, readable, and performant.

## 1. Core Principles

### 1.1 Interpreter & Strict Mode
Always specify `zsh` as the interpreter and enable strict mode for early error detection.

✅ **GOOD:**
```zsh
#!/usr/bin/env zsh

# Enable strict mode:
# errreturn: Exit immediately if a command exits with a non-zero status.
# nounset: Treat unset variables as an error.
# pipefail: The exit status of a pipeline is the status of the last command to exit with a non-zero status, or zero if all commands exit successfully.
setopt errreturn nounset pipefail
```

❌ **BAD:**
```zsh
#!/bin/bash # Wrong interpreter
# No strict mode options
```

### 1.2 ShellCheck Integration
ShellCheck is non-negotiable. Integrate it into your editor and CI pipeline.

✅ **GOOD:**
```zsh
# .github/workflows/ci.yml
name: ShellCheck

on: [push, pull_request]

jobs:
  shellcheck:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Run ShellCheck
      uses: ludeeus/action-shellcheck@v2
      with:
        scandir: './' # Scan all shell scripts in the repository
```

## 2. Code Organization and Structure

### 2.1 File Header & Comments
Every script must start with a descriptive header. Functions require clear comments.

✅ **GOOD:**
```zsh
#!/usr/bin/env zsh
#
# Description: Manages project dependencies via a custom tool.
#
# setopt errreturn nounset pipefail

#######################################
# Fetches and installs project dependencies.
# Globals:
#   PROJECT_ROOT
# Arguments:
#   None
# Outputs:
#   Writes status messages to stderr.
# Returns:
#   0 if dependencies are installed, non-zero on error.
#######################################
function install_dependencies() {
  # ... implementation ...
}
```

### 2.2 Function Location & `main`
Define all functions before their first use. Use a `main` function for the script's entry point.

✅ **GOOD:**
```zsh
#!/usr/bin/env zsh
setopt errreturn nounset pipefail

function _log_info() {
  echo "[INFO] $*" >&2
}

function _process_data() {
  _log_info "Processing data for: $1"
  # ...
}

function main() {
  _log_info "Starting data processing..."
  _process_data "report_A"
  _process_data "report_B"
  _log_info "Processing complete."
}

main "$@"
```

## 3. Formatting

### 3.1 Indentation & Line Length
Indent with 2 spaces. Limit lines to 80 characters.

✅ **GOOD:**
```zsh
if [[ -n "${VAR}" ]]; then
  echo "Variable is set."
fi

long_command_name --option-one value \
  --option-two another-value \
  --final-option
```

❌ **BAD:**
```zsh
if [[ -n "${VAR}" ]]; then
    echo "Variable is set."
fi # 4 spaces

long_command_name --option-one value --option-two another-value --final-option # Too long
```

### 3.2 Quoting & Variable Expansion
Always quote variables and command substitutions unless you explicitly need word splitting. Prefer `${parameter}`.

✅ **GOOD:**
```zsh
local my_file="report.txt"
local content="$(cat "${my_file}")" # Command substitution quoted
echo "Processing file: ${my_file}"  # Variable quoted, preferred syntax
```

❌ **BAD:**
```zsh
local my_file=report.txt
local content=$(cat $my_file) # Unquoted, prone to word splitting/globbing
echo "Processing file: $my_file" # Legacy syntax
```

## 4. Naming Conventions

### 4.1 Functions & Variables
- **Functions:** `lower_snake_case`. Prefix internal functions with `_`.
- **Constants/Environment Variables:** `ALL_CAPS`.
- **Local Variables:** `local lower_snake_case`.

✅ **GOOD:**
```zsh
local MY_CONSTANT="fixed_value"
local _internal_helper_var="temp"

function calculate_total() {
  local item_count="${1}"
  local unit_price="${2}"
  echo $(( item_count * unit_price ))
}
```

❌ **BAD:**
```zsh
local myConstant="value" # CamelCase
function CalculateTotal() { # PascalCase
  local ItemCount="${1}" # PascalCase
}
```

## 5. Zsh-Specific Patterns

### 5.1 Zsh Arrays
Use Zsh's native array handling. `${array}` expands to all elements. Use `"${(@)array}"` to preserve empty entries.

✅ **GOOD:**
```zsh
local my_array=("apple" "" "banana")
echo "All elements: ${my_array}"        # apple banana (empty removed)
echo "Preserving empty: ${(@)my_array}" # apple  banana (empty preserved)

# Iterate safely
for item in "${(@)my_array}"; do
  echo "Item: '${item}'"
done
```

❌ **BAD:**
```zsh
local my_array=("apple" "" "banana")
echo "Bash style: ${my_array[@]}" # Works, but not idiomatic Zsh
```

### 5.2 Extended Globbing
Enable `extended_glob` for powerful pattern matching.

✅ **GOOD:**
```zsh
setopt extended_glob

# Find files not ending with .txt or .log
ls -d *~*.(txt|log)

# Case-insensitive glob
ls -d (#i)*.txt
```

### 5.3 Parameter Expansion for Path Manipulation
Avoid `dirname` and `basename` external commands.

✅ **GOOD:**
```zsh
local full_path="/path/to/my/file.txt"
local dir_name="${full_path:h}"  # /path/to/my
local base_name="${full_path:t}" # file.txt
local absolute_path="${full_path:A}" # Resolve symlinks and get absolute path
```

❌ **BAD:**
```zsh
local full_path="/path/to/my/file.txt"
local dir_name="$(dirname "${full_path}")"
local base_name="$(basename "${full_path}")"
```

### 5.4 Zsh-Native Filtering (Skipping `grep`/`tr`)
Leverage Zsh's parameter expansion for filtering and transformations.

✅ **GOOD:**
```zsh
# Filter array (like grep)
local lines=("line one" "another line" "third line")
local matched_lines=(${(M)lines:#*line*}) # Matches lines containing "line"
print -l "${matched_lines[@]}"

# Transform string (like tr)
local text="hello world"
local transformed_text="${text//[aeiou]/_}" # h_ll_ w_rld
```

❌ **BAD:**
```zsh
local lines=("line one" "another line" "third line")
local matched_lines="$(printf "%s\n" "${lines[@]}" | grep "line")" # External command, subshell
```

### 5.5 Ternary Expressions
Use `:+` and `:-` for concise conditional assignments.

✅ **GOOD:**
```zsh
local debug_mode="true"
local log_level="${debug_mode:+DEBUG}:INFO" # If debug_mode is set, prepend DEBUG
echo "Log level: ${log_level}" # Output: Log level: DEBUG:INFO

local debug_mode=""
local log_level="${debug_mode:+DEBUG}:INFO"
echo "Log level: ${log_level}" # Output: Log level: :INFO
```

## 6. Anti-Patterns & Pitfalls

### 6.1 Avoid `eval`
`eval` is a security risk and makes code hard to debug. Never use it.

❌ **BAD:**
```zsh
local cmd="ls -l"
eval "${cmd}" # Dangerous!
```

### 6.2 `source` with Caution
Only `source` trusted files. For external tools, prefer explicit execution or `eval "$(tool init zsh)"` if the tool is trusted and designed for it.

✅ **GOOD:**
```zsh
# .zshrc
# Source trusted local configuration
source "${ZDOTDIR:-$HOME}/.zsh_aliases"

# For trusted tools that provide init scripts
eval "$(zoxide init zsh)"
```

❌ **BAD:**
```zsh
source "/tmp/untrusted_script.zsh"
```

### 6.3 Minimal `.zshrc`
Keep your `.zshrc` lean. Avoid heavy frameworks like Oh My Zsh unless absolutely necessary. Prefer lightweight tools and manual configuration.

✅ **GOOD:**
```zsh
# .zshrc
HISTFILE=~/.zsh_history
HISTSIZE=100000
SAVEHIST=100000
setopt HIST_SAVE_NO_DUPS INC_APPEND_HISTORY AUTO_PUSHD PUSHD_IGNORE_DUPS PUSHD_SILENT autocd
autoload -U compinit; compinit

# Prompt via Starship (external, lightweight)
eval "$(starship init zsh)"
```

❌ **BAD:**
```zsh
# .zshrc
# Too many plugins, slow startup
source $ZSH/oh-my-zsh.sh
plugins=(git docker web-search history-substring-search ...)
```

© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc

Similar IDE rules

IDE rules catalog →

From the same set240

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor