From 4cfec0b336dbad776a5764066fb5e98d8bd7796d Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Fri, 6 Mar 2026 11:57:24 +0100 Subject: [PATCH] Initial commit: modular bash configuration Reinitialised repo to purge credential history. Credential files are now gitignored with .example templates. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 6 +++ 00-credential-guard | 19 +++++++ 00-path-helper | 13 +++++ 10-bun-path | 2 + 10-go-path | 3 ++ 10-rust-path | 3 ++ 20-ninja | 9 ++++ 20-oneapi | 13 +++++ 30-starship | 6 +++ 50-asdf-completion | 3 ++ 50-claude-completion | 72 +++++++++++++++++++++++++ 50-fj-completion | 3 ++ 50-tailscale-completion | 3 ++ 50-uv-completion | 3 ++ 99-android | 9 ++++ 99-claude.example | 4 ++ 99-gemini.example | 4 ++ 99-google.example | 4 ++ 99-huggingface.example | 4 ++ 99-replicate.example | 4 ++ CLAUDE.md | 63 ++++++++++++++++++++++ README.md | 114 ++++++++++++++++++++++++++++++++++++++++ 22 files changed, 364 insertions(+) create mode 100644 .gitignore create mode 100755 00-credential-guard create mode 100755 00-path-helper create mode 100755 10-bun-path create mode 100755 10-go-path create mode 100755 10-rust-path create mode 100755 20-ninja create mode 100755 20-oneapi create mode 100755 30-starship create mode 100755 50-asdf-completion create mode 100755 50-claude-completion create mode 100755 50-fj-completion create mode 100755 50-tailscale-completion create mode 100755 50-uv-completion create mode 100755 99-android create mode 100644 99-claude.example create mode 100644 99-gemini.example create mode 100644 99-google.example create mode 100644 99-huggingface.example create mode 100644 99-replicate.example create mode 100644 CLAUDE.md create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a5b8c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Credential files — never commit real secrets +99-claude +99-gemini +99-google +99-huggingface +99-replicate diff --git a/00-credential-guard b/00-credential-guard new file mode 100755 index 0000000..f1c122e --- /dev/null +++ b/00-credential-guard @@ -0,0 +1,19 @@ +# shellcheck shell=bash +# Helper to warn if a credential file has loose permissions + +# require_private +# Emits a warning to stderr if the file is group- or world-readable. +require_private() { + local file="$1" + [[ -f "$file" ]] || return 0 + local perms + perms=$(stat -c %a "$file" 2>/dev/null) || { + echo "bash.d: WARNING: cannot check permissions on $file (stat failed)" >&2 + return 1 + } + # Check that group and other bits are both zero (e.g., 600, 700) + # Uses arithmetic on octal value to handle both 3- and 4-digit modes + if (( (8#$perms) & 8#077 )); then + echo "bash.d: WARNING: $file is group/world-accessible (mode $perms). Run: chmod 600 $file" >&2 + fi +} diff --git a/00-path-helper b/00-path-helper new file mode 100755 index 0000000..5054f2a --- /dev/null +++ b/00-path-helper @@ -0,0 +1,13 @@ +# shellcheck shell=bash +# Helper functions to safely modify PATH +# Both check that the directory exists AND prevent duplicates on re-source + +# Append directory to PATH (lower priority than existing entries) +path_append() { + [[ -d "$1" ]] && [[ ":$PATH:" != *":$1:"* ]] && PATH="$PATH:$1" +} + +# Prepend directory to PATH (higher priority than existing entries) +path_prepend() { + [[ -d "$1" ]] && [[ ":$PATH:" != *":$1:"* ]] && PATH="$1:$PATH" +} diff --git a/10-bun-path b/10-bun-path new file mode 100755 index 0000000..006ab9d --- /dev/null +++ b/10-bun-path @@ -0,0 +1,2 @@ +# shellcheck shell=bash +path_append "$HOME/.bun/bin" diff --git a/10-go-path b/10-go-path new file mode 100755 index 0000000..6e4c1ed --- /dev/null +++ b/10-go-path @@ -0,0 +1,3 @@ +# shellcheck shell=bash +export GOPATH="$HOME/go" +path_append "$GOPATH/bin" diff --git a/10-rust-path b/10-rust-path new file mode 100755 index 0000000..102068b --- /dev/null +++ b/10-rust-path @@ -0,0 +1,3 @@ +# shellcheck shell=bash +export CARGO_HOME="$HOME/.cargo" +path_append "$CARGO_HOME/bin" diff --git a/20-ninja b/20-ninja new file mode 100755 index 0000000..2200658 --- /dev/null +++ b/20-ninja @@ -0,0 +1,9 @@ +# shellcheck shell=bash +if command -v ninja &>/dev/null; then + export NINJA_STATUS="[%e sec | %p (%u remaining) | %o / sec] " + export CMAKE_GENERATOR=Ninja +fi +if command -v ccache &>/dev/null; then + export CMAKE_C_COMPILER_LAUNCHER=ccache + export CMAKE_CXX_COMPILER_LAUNCHER=ccache +fi diff --git a/20-oneapi b/20-oneapi new file mode 100755 index 0000000..0b007eb --- /dev/null +++ b/20-oneapi @@ -0,0 +1,13 @@ +# shellcheck shell=bash +# Intel oneAPI toolkit — source the environment setup if installed +# shellcheck disable=SC1090 +ONEAPI_SETVARS="/opt/intel/oneapi/setvars.sh" +if [[ -f "$ONEAPI_SETVARS" ]]; then + . "$ONEAPI_SETVARS" --force > /dev/null + if [[ -n "$ONEAPI_ROOT" ]]; then + case ":${LD_LIBRARY_PATH}:" in + *":${ONEAPI_ROOT}/lib:"*) ;; + *) export LD_LIBRARY_PATH="${ONEAPI_ROOT}/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" ;; + esac + fi +fi diff --git a/30-starship b/30-starship new file mode 100755 index 0000000..8429684 --- /dev/null +++ b/30-starship @@ -0,0 +1,6 @@ +# shellcheck shell=bash +# Initialize Starship prompt +# Requires: starship binary in PATH (installed to ~/.local/bin) +if command -v starship &>/dev/null; then + eval "$(starship init bash)" +fi diff --git a/50-asdf-completion b/50-asdf-completion new file mode 100755 index 0000000..99e080c --- /dev/null +++ b/50-asdf-completion @@ -0,0 +1,3 @@ +# shellcheck shell=bash +# shellcheck disable=SC1090 +command -v asdf &>/dev/null && . <(asdf completion bash) diff --git a/50-claude-completion b/50-claude-completion new file mode 100755 index 0000000..8657ca4 --- /dev/null +++ b/50-claude-completion @@ -0,0 +1,72 @@ +# shellcheck shell=bash +# shellcheck disable=SC2207 # Standard pattern for bash completions +# Bash completion for Claude Code CLI + +command -v claude &>/dev/null || return + +_claude_completions() { + local cur prev opts commands + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + # Subcommands + commands="doctor install mcp plugin setup-token update" + + # All options + opts="-c -d -h -p -r -v + --add-dir --agent --agents --allow-dangerously-skip-permissions + --allowed-tools --append-system-prompt --betas --chrome --continue + --dangerously-skip-permissions --debug --debug-file --disable-slash-commands + --disallowed-tools --fallback-model --file --fork-session --help --ide + --include-partial-messages --input-format --json-schema --max-budget-usd + --mcp-config --model --no-chrome --no-session-persistence --output-format + --permission-mode --plugin-dir --print --replay-user-messages --resume + --session-id --setting-sources --settings --strict-mcp-config + --system-prompt --tools --verbose --version" + + # Handle option arguments + case "${prev}" in + --model|--fallback-model) + COMPREPLY=( $(compgen -W "sonnet opus haiku" -- "${cur}") ) + return 0 + ;; + --permission-mode) + COMPREPLY=( $(compgen -W "acceptEdits bypassPermissions default delegate dontAsk plan" -- "${cur}") ) + return 0 + ;; + --input-format) + COMPREPLY=( $(compgen -W "text stream-json" -- "${cur}") ) + return 0 + ;; + --output-format) + COMPREPLY=( $(compgen -W "text json stream-json" -- "${cur}") ) + return 0 + ;; + --add-dir|--plugin-dir|--debug-file) + COMPREPLY=( $(compgen -d -- "${cur}") ) + return 0 + ;; + --settings|--mcp-config) + COMPREPLY=( $(compgen -f -- "${cur}") ) + return 0 + ;; + install) + COMPREPLY=( $(compgen -W "stable latest" -- "${cur}") ) + return 0 + ;; + esac + + # Complete subcommands or options + if [[ ${cur} == -* ]]; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + elif [[ ${COMP_CWORD} -eq 1 ]]; then + COMPREPLY=( $(compgen -W "${commands} ${opts}" -- "${cur}") ) + else + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + fi + + return 0 +} + +complete -F _claude_completions claude diff --git a/50-fj-completion b/50-fj-completion new file mode 100755 index 0000000..60070b3 --- /dev/null +++ b/50-fj-completion @@ -0,0 +1,3 @@ +# shellcheck shell=bash +# shellcheck disable=SC1090 +command -v fj &>/dev/null && . <(fj completion bash) diff --git a/50-tailscale-completion b/50-tailscale-completion new file mode 100755 index 0000000..7469802 --- /dev/null +++ b/50-tailscale-completion @@ -0,0 +1,3 @@ +# shellcheck shell=bash +# shellcheck disable=SC1090 +command -v tailscale &>/dev/null && . <(tailscale completion bash) diff --git a/50-uv-completion b/50-uv-completion new file mode 100755 index 0000000..b4ac12b --- /dev/null +++ b/50-uv-completion @@ -0,0 +1,3 @@ +# shellcheck shell=bash +# Enable bash completion for uv (Python package manager) +command -v uv &>/dev/null && eval "$(uv generate-shell-completion bash)" diff --git a/99-android b/99-android new file mode 100755 index 0000000..29b04eb --- /dev/null +++ b/99-android @@ -0,0 +1,9 @@ +# shellcheck shell=bash +if [[ -d "$HOME/android-sdk" ]]; then + export ANDROID_HOME="$HOME/android-sdk" + path_append "$ANDROID_HOME/cmdline-tools/latest/bin" + path_append "$ANDROID_HOME/platform-tools" +fi +if [[ -d /usr/lib/jvm/java-17-openjdk-amd64 ]]; then + export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 +fi diff --git a/99-claude.example b/99-claude.example new file mode 100644 index 0000000..a17fdb5 --- /dev/null +++ b/99-claude.example @@ -0,0 +1,4 @@ +# shellcheck shell=bash +# Copy to 99-claude and fill in your token, then: chmod 700 99-claude +require_private "${BASH_SOURCE[0]}" +export FORGEJO_ISSUE_TOKEN_FOR_CLAUDE=your-token-here diff --git a/99-gemini.example b/99-gemini.example new file mode 100644 index 0000000..91fe52d --- /dev/null +++ b/99-gemini.example @@ -0,0 +1,4 @@ +# shellcheck shell=bash +# Copy to 99-gemini and fill in your key, then: chmod 700 99-gemini +require_private "${BASH_SOURCE[0]}" +export GEMINI_API_KEY=your-key-here diff --git a/99-google.example b/99-google.example new file mode 100644 index 0000000..97f7840 --- /dev/null +++ b/99-google.example @@ -0,0 +1,4 @@ +# shellcheck shell=bash +# Copy to 99-google and fill in your key, then: chmod 700 99-google +require_private "${BASH_SOURCE[0]}" +export GOOGLE_API_KEY=your-key-here diff --git a/99-huggingface.example b/99-huggingface.example new file mode 100644 index 0000000..1b2dbba --- /dev/null +++ b/99-huggingface.example @@ -0,0 +1,4 @@ +# shellcheck shell=bash +# Copy to 99-huggingface and fill in your token, then: chmod 700 99-huggingface +require_private "${BASH_SOURCE[0]}" +export HF_TOKEN=your-token-here diff --git a/99-replicate.example b/99-replicate.example new file mode 100644 index 0000000..9dc9d0a --- /dev/null +++ b/99-replicate.example @@ -0,0 +1,4 @@ +# shellcheck shell=bash +# Copy to 99-replicate and fill in your token, then: chmod 700 99-replicate +require_private "${BASH_SOURCE[0]}" +export REPLICATE_API_TOKEN=your-token-here diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2185ee7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,63 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +This is a modular bash configuration directory (`~/.bash.d/`). Files here are sourced by `~/.bashrc` to set up the shell environment. Each file handles a specific concern (PATH additions, environment variables, shell completions). See `README.md` for a full description of the directory and its contents. + +## File Naming Convention + +Files use numeric prefixes to control load order: +- **00-** : Helper functions (loads first — used by other scripts) +- **10-** : PATH configuration (foundational) +- **20-** : Build tool settings (depends on paths) +- **30-** : Shell/prompt setup +- **50-** : Shell completions (named `50--completion`) +- **99-** : Application config, credentials (loads last) + +## Available Helpers (defined in `00-*`) + +Use these in all scripts — do not manipulate `PATH` or check permissions manually: +- `path_append ` — add directory to end of `$PATH` (checks existence, prevents duplicates) +- `path_prepend ` — add directory to start of `$PATH` +- `require_private ` — warn if file is group/world-accessible; use in credential files + +## Writing Scripts + +- Every file must start with `# shellcheck shell=bash` +- Guard external tools with `command -v &>/dev/null` before using them +- Guard directory-dependent exports with `[[ -d ]]` before exporting +- Do not suppress stderr from external scripts — only redirect stdout when needed +- Avoid `eval` when `. <(cmd)` (process substitution) works +- Prevent `LD_LIBRARY_PATH` duplicates with a `case` guard (see `20-oneapi` for example) + +## Permissions + +- Directory `~/.bash.d/` itself: mode `700` +- Regular scripts: mode `755` (executable is **required** for sourcing) +- Credential files (`99-*`): mode `700` and must call `require_private "${BASH_SOURCE[0]}"` as the first functional line + +## Validation + +Validate all shell scripts with shellcheck before committing: +```bash +shellcheck +``` + +## Security + +Credential files are **excluded from git** via `.gitignore`. Each has a tracked `.example` template with placeholder values. + +To set up a credential: +1. Copy the template: `cp 99-foo.example 99-foo` +2. Fill in your real secret +3. Restrict permissions: `chmod 700 99-foo` + +When adding new credential files: +- Create a `.example` template (mode `644`) tracked in git +- Add the real filename to `.gitignore` +- Use mode `700` on the real file: `chmod 700 ` +- Call `require_private "${BASH_SOURCE[0]}"` as the first functional line +- **Never commit real secrets** +- Be aware that `export`ed tokens are visible to all child processes diff --git a/README.md b/README.md new file mode 100644 index 0000000..1523257 --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# ~/.bash.d — Modular Bash Configuration + +This directory contains modular shell configuration files that are automatically sourced by `~/.bashrc` on every new shell session. Instead of maintaining one monolithic `.bashrc`, each concern lives in its own file — making the setup easier to understand, maintain, and extend. + +## How It Works + +The `~/.bashrc` iterates over all files in `~/.bash.d/` and sources each one that has the **executable bit** set. Non-executable files are silently skipped, which provides a simple way to temporarily disable a configuration (just `chmod -x` the file). + +Files are sourced in **lexicographic order**, so numeric prefixes control the load sequence. This matters because later files may depend on functions or variables defined by earlier ones. + +## File Naming Convention + +| Prefix | Purpose | Example | +|--------|----------------------------------|---------------------------| +| `00-` | Helper functions (loaded first) | `00-path-helper` | +| `10-` | PATH configuration | `10-go-path`, `10-rust-path` | +| `20-` | Build tool settings | `20-ninja` | +| `30-` | Shell/prompt setup | `30-starship` | +| `50-` | Shell completions | `50-claude-completion` | +| `99-` | Credentials and app config (last)| `99-gemini`, `99-huggingface` | + +Lower numbers load first, so foundational pieces like helper functions (`00-`) and PATH entries (`10-`) are available before anything that depends on them. + +## Current Files + +### Helpers (`00-`) + +- **`00-path-helper`** — Defines `path_append` and `path_prepend` functions that safely add directories to `$PATH` (checking the directory exists and avoiding duplicates). +- **`00-credential-guard`** — Defines `require_private`, which warns at shell startup if a credential file has overly permissive permissions (anything beyond owner-only access). + +### PATH (`10-`) + +- **`10-bun-path`** — Adds Bun (JavaScript runtime) binaries to PATH. +- **`10-go-path`** — Sets `$GOPATH` and adds Go binaries to PATH. +- **`10-rust-path`** — Sets `$CARGO_HOME` and adds Cargo binaries to PATH. + +### Build Tools (`20-`) + +- **`20-ninja`** — Configures Ninja as the default CMake generator and enables ccache for C/C++ builds (only when installed). +- **`20-oneapi`** — Sources the Intel oneAPI environment and adds its libraries to `LD_LIBRARY_PATH`. + +### Prompt (`30-`) + +- **`30-starship`** — Initialises the [Starship](https://starship.rs) cross-shell prompt, if installed. + +### Completions (`50-`) + +- **`50-asdf-completion`** — Tab completion for the asdf version manager. +- **`50-claude-completion`** — Tab completion for [Claude Code](https://claude.com/claude-code) CLI. +- **`50-fj-completion`** — Tab completion for the Forgejo CLI (`fj`). +- **`50-tailscale-completion`** — Tab completion for Tailscale. +- **`50-uv-completion`** — Tab completion for [uv](https://github.com/astral-sh/uv) (Python package manager). + +### Credentials & App Config (`99-`) + +- **`99-android`** — Sets `$ANDROID_HOME`, `$JAVA_HOME`, and adds Android SDK tools to PATH (only when installed). +- **`99-claude.example`** — Template for Forgejo issue token for Claude Code integrations. +- **`99-gemini.example`** — Template for Gemini API key. +- **`99-google.example`** — Template for Google API key. +- **`99-huggingface.example`** — Template for HuggingFace token (`$HF_TOKEN`). +- **`99-replicate.example`** — Template for Replicate API token. + +## Adding a New File + +1. Create the file with the appropriate numeric prefix: + ```bash + vim ~/.bash.d/50-mytool-completion + ``` + +2. Start the file with the shellcheck directive: + ```bash + # shellcheck shell=bash + ``` + +3. Make it executable (required for it to be sourced): + ```bash + chmod +x ~/.bash.d/50-mytool-completion + ``` + +4. For credential files, create a `.example` template and the real file: + ```bash + # Create the template (tracked in git) + cat > ~/.bash.d/99-mytool.example << 'EOF' + # shellcheck shell=bash + # Copy to 99-mytool and fill in your token, then: chmod 700 99-mytool + require_private "${BASH_SOURCE[0]}" + export MY_SECRET_TOKEN=your-token-here + EOF + + # Create the real file from the template + cp ~/.bash.d/99-mytool.example ~/.bash.d/99-mytool + # Edit 99-mytool and fill in your real secret + chmod 700 ~/.bash.d/99-mytool + ``` + Then add `99-mytool` to `.gitignore`. + +5. Validate with shellcheck: + ```bash + shellcheck ~/.bash.d/50-mytool-completion + ``` + +## Security + +Credential files (`99-*`) are **excluded from git** via `.gitignore` and are never committed. Each credential has a tracked `.example` template with placeholder values. To set up credentials: + +1. Copy the template: `cp 99-foo.example 99-foo` +2. Fill in your real secret +3. Restrict permissions: `chmod 700 99-foo` + +Additional protections: + +- **File permissions** — Credential files use mode `700` (owner-only). +- **Runtime checks** — The `require_private` helper warns on shell startup if a credential file has been accidentally loosened. +- **Never commit real secrets** — Only `.example` templates are tracked in git.