diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..0dc1b50 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,112 @@ +name: Security + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + secrets: + name: Secret Scanning + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Scan for leaked secrets + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Lint shell scripts + uses: ludeeus/action-shellcheck@master + with: + scandir: . + ignore_names: zshrc + + dangerous-patterns: + name: Dangerous Patterns + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check for destructive commands + run: | + echo "Scanning for dangerous patterns..." + EXIT_CODE=0 + + # Patterns to flag (with context) + PATTERNS=( + 'rm -rf /' + 'rm -rf ~' + 'rm -rf \$HOME' + 'rm -rf \*' + 'mkfs\.' + 'dd if=' + ':(){:|:&};:' + 'chmod -R 777' + 'chmod 777' + '> /dev/sda' + 'curl.*\|.*sudo' + 'wget.*\|.*sudo' + ) + + for pattern in "${PATTERNS[@]}"; do + # Search all tracked files, skip this workflow file itself + MATCHES=$(grep -rn --include='*.sh' --include='*.zsh' --include='zshrc' --include='*.toml' \ + -E "$pattern" . \ + --exclude-dir=.git \ + --exclude='.github/workflows/security.yml' \ + 2>/dev/null || true) + + if [[ -n "$MATCHES" ]]; then + echo "::warning::Found potentially dangerous pattern: $pattern" + echo "$MATCHES" + EXIT_CODE=1 + fi + done + + # Check for unquoted variables in shell scripts (common security issue) + UNQUOTED=$(grep -rn --include='*.sh' \ + -E 'rm.*\$[A-Z_]+[^"'"'"']' . \ + --exclude-dir=.git \ + 2>/dev/null || true) + + if [[ -n "$UNQUOTED" ]]; then + echo "::warning::Found potentially unquoted variables in rm commands" + echo "$UNQUOTED" + EXIT_CODE=1 + fi + + if [[ $EXIT_CODE -eq 0 ]]; then + echo "No dangerous patterns found." + fi + + exit $EXIT_CODE + + syntax: + name: Syntax Validation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate bash scripts + run: | + echo "Checking bash syntax..." + bash -n install.sh + bash -n deploy.sh + echo "All bash scripts pass syntax check." + + - name: Validate zsh syntax + run: | + echo "Checking zsh syntax..." + zsh -n zshrc || echo "::warning::zshrc has syntax issues (may require oh-my-zsh sourced)" + echo "Done." diff --git a/AGENTS.md b/AGENTS.md index 228024d..af58b3e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,9 +47,14 @@ zsh -n zshrc # zsh: source ~/.zshrc or open new terminal ``` -### No Build/Test/Lint Commands +### CI - GitHub Actions -This repo has no build system, test suite, automated linting, or CI/CD pipeline. +A security workflow runs on push to `main` and on pull requests (`.github/workflows/security.yml`): + +- **Secret scanning** - gitleaks detects leaked tokens, API keys, passwords +- **ShellCheck** - lints `install.sh`, `deploy.sh`, `zshrc` for unsafe shell patterns +- **Dangerous patterns** - flags `rm -rf /`, `chmod 777`, `curl | sudo`, etc. +- **Syntax validation** - runs `bash -n` and `zsh -n` on scripts ## File Structure @@ -57,6 +62,7 @@ This repo has no build system, test suite, automated linting, or CI/CD pipeline. . ├── install.sh # Install required apps and tools ├── deploy.sh # Deploy configs to home directory +├── .github/workflows/ # CI security checks (gitleaks, shellcheck) ├── aerospace.toml # AeroSpace tiling window manager ├── config.ghostty # Ghostty terminal emulator ├── gitconfig # Git configuration @@ -138,7 +144,6 @@ nvm() { _nvm_load; nvm "$@"; } | Aliases | Short, lowercase | `gm`, `nd`, `vim` | | Functions | snake_case | `load_nvmrc`, `_nvm_load` | | Environment vars | UPPER_SNAKE_CASE | `NVM_DIR`, `FZF_DEFAULT_COMMAND` | -| Lua variables | snake_case | `lazy_path`, `nvmrc_node_version` | | tmux options | kebab-case | `status-left-length`, `pane-border-status` | ## Key Architecture Decisions @@ -156,6 +161,7 @@ nvm() { _nvm_load; nvm "$@"; } - **Don't commit .zshenv**: Only `zshenv.example` is tracked; real secrets stay local - **Don't modify LazyVim core**: Add overrides in `lua/plugins/`, import extras properly - **Check deployment**: After editing, run `./deploy.sh ` to apply changes +- **Don't add dangerous patterns**: CI flags `rm -rf /`, `chmod 777`, `curl | sudo`, etc. ## File-Specific Notes @@ -167,9 +173,7 @@ nvm() { _nvm_load; nvm "$@"; } ### tmux.conf - TPM (plugin manager) installed via `./install.sh` - After changes: `Ctrl-a + I` to install/update plugins -- Catppuccin Macchiato theme with custom status bar ### zshrc - Profile startup: uncomment `zmodload zsh/zprof` at top and `zprof` at bottom - Oh-My-Zsh plugins minimized for speed - add sparingly -- FZF integration uses `fd` for file finding diff --git a/CLAUDE.md b/CLAUDE.md index ad4a157..4aa1e33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,26 +20,14 @@ A macOS dotfiles repository. Run `./install.sh` to install apps, then `./deploy. `deploy.sh` backs up existing configs to `~/.dotfiles-backup//` before overwriting. Warns if a target app is not installed (but still copies the config). -## Config Files and Their Targets - -| Repo file | Deploys to | Notes | -|---|---|---| -| `aerospace.toml` | `~/.aerospace.toml` | AeroSpace tiling WM. Reload: `alt-shift-;` then `esc` | -| `tmux.conf` | `~/.tmux.conf` | Prefix is `Ctrl-a`. Install plugins: `Ctrl-a + I` | -| `zshrc` | `~/.zshrc` | Oh-My-Zsh with lazy-loaded NVM/Conda | -| `zshenv.example` | `~/.zshenv` | Template for private tokens (never commit real values) | -| `gitconfig` | `~/.gitconfig` | Uses nvim as editor, rebase on pull | -| `config.ghostty` | Ghostty config | Catppuccin Mocha, 75% opacity | -| `nvim/` | `~/.config/nvim/` | LazyVim framework | -| `vscode-settings-user.json` | VS Code user settings | | - ## Conventions - **Theme**: Catppuccin (Mocha/Macchiato) across all tools - **TOML formatting**: The nvim TOML formatter strips section indentation to flat style — this is expected and valid -- **Secrets**: Go in `~/.zshenv` (from `zshenv.example`), never committed. The `.gitignore` only excludes `.DS_Store` +- **Secrets**: Go in `~/.zshenv` (from `zshenv.example`), never committed - **Commit messages**: Use conventional format with scope — `feat(tmux): ...`, `ref(zsh): ...`, `chore: ...` - **Zsh performance**: NVM and Conda are lazy-loaded; completions are cached in `zsh/` directory. Don't add eager-loading of heavy tools +- **CI**: Security workflow runs on push/PR — gitleaks, ShellCheck, dangerous pattern scan, syntax validation ## Key Architecture Decisions diff --git a/deploy.sh b/deploy.sh index 20cec99..5fbee62 100755 --- a/deploy.sh +++ b/deploy.sh @@ -178,7 +178,7 @@ deploy() { case "$target" in zsh) if cp "$SCRIPT_DIR/zshrc" "$HOME/.zshrc" 2>/dev/null; then - success "zsh" "zshrc" "~/.zshrc" + success "zsh" "zshrc" "$HOME/.zshrc" DEPLOYED=$((DEPLOYED + 1)) else fail "zsh" "failed to copy zshrc" @@ -189,7 +189,7 @@ deploy() { nvim) mkdir -p "$HOME/.config" if cp -r "$SCRIPT_DIR/nvim/" "$HOME/.config/nvim/" 2>/dev/null; then - success "nvim" "nvim/" "~/.config/nvim/" + success "nvim" "nvim/" "$HOME/.config/nvim/" DEPLOYED=$((DEPLOYED + 1)) else fail "nvim" "failed to copy nvim/" @@ -199,7 +199,7 @@ deploy() { tmux) if cp "$SCRIPT_DIR/tmux.conf" "$HOME/.tmux.conf" 2>/dev/null; then - success "tmux" "tmux.conf" "~/.tmux.conf" + success "tmux" "tmux.conf" "$HOME/.tmux.conf" DEPLOYED=$((DEPLOYED + 1)) else fail "tmux" "failed to copy tmux.conf" @@ -209,7 +209,7 @@ deploy() { aerospace) if cp "$SCRIPT_DIR/aerospace.toml" "$HOME/.aerospace.toml" 2>/dev/null; then - success "aerospace" "aerospace.toml" "~/.aerospace.toml" + success "aerospace" "aerospace.toml" "$HOME/.aerospace.toml" DEPLOYED=$((DEPLOYED + 1)) else fail "aerospace" "failed to copy aerospace.toml" @@ -220,7 +220,7 @@ deploy() { ghostty) mkdir -p "$HOME/.config/ghostty" if cp "$SCRIPT_DIR/config.ghostty" "$HOME/.config/ghostty/config" 2>/dev/null; then - success "ghostty" "config.ghostty" "~/.config/ghostty/config" + success "ghostty" "config.ghostty" "$HOME/.config/ghostty/config" DEPLOYED=$((DEPLOYED + 1)) else fail "ghostty" "failed to copy config.ghostty" @@ -230,7 +230,7 @@ deploy() { git) if cp "$SCRIPT_DIR/gitconfig" "$HOME/.gitconfig" 2>/dev/null; then - success "git" "gitconfig" "~/.gitconfig" + success "git" "gitconfig" "$HOME/.gitconfig" DEPLOYED=$((DEPLOYED + 1)) else fail "git" "failed to copy gitconfig" @@ -242,7 +242,7 @@ deploy() { local vscode_dir="$HOME/Library/Application Support/Code/User" mkdir -p "$vscode_dir" if cp "$SCRIPT_DIR/vscode-settings-user.json" "$vscode_dir/settings.json" 2>/dev/null; then - success "vscode" "vscode-settings-user.json" "~/Library/.../settings.json" + success "vscode" "vscode-settings-user.json" "$HOME/Library/.../settings.json" DEPLOYED=$((DEPLOYED + 1)) else fail "vscode" "failed to copy vscode settings" @@ -252,12 +252,12 @@ deploy() { zshenv) if [[ -f "$HOME/.zshenv" ]]; then - warn "zshenv" "skipped (${DIM}~/.zshenv already exists${RST}${YELLOW})${RST}" + warn "zshenv" "skipped (${DIM}\$HOME/.zshenv already exists${RST}${YELLOW})${RST}" SKIPPED=$((SKIPPED + 1)) else if cp "$SCRIPT_DIR/zshenv.example" "$HOME/.zshenv" 2>/dev/null; then chmod 600 "$HOME/.zshenv" - success "zshenv" "zshenv.example" "~/.zshenv" + success "zshenv" "zshenv.example" "$HOME/.zshenv" echo -e " ${YELLOW}fill in your tokens and keep chmod 600${RST}" DEPLOYED=$((DEPLOYED + 1)) else diff --git a/install.sh b/install.sh index 1a197eb..2915c16 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,6 @@ FAILED=0 if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then GREEN='\033[38;2;166;227;161m' # #a6e3a1 RED='\033[38;2;243;139;168m' # #f38ba8 - YELLOW='\033[38;2;249;226;175m' # #f9e2af BLUE='\033[38;2;137;180;250m' # #89b4fa MAUVE='\033[38;2;203;166;247m' # #cba6f7 DIM='\033[38;2;108;112;134m' # #6c7086 overlay0 @@ -26,7 +25,7 @@ if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then BOLD='\033[1m' RST='\033[0m' else - GREEN='' RED='' YELLOW='' BLUE='' MAUVE='' DIM='' SUB='' BOLD='' RST='' + GREEN='' RED='' BLUE='' MAUVE='' DIM='' SUB='' BOLD='' RST='' fi # =============================================================================