Claude-skill-registry state-directory-manager
Manage persistent state directories for bash scripts
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/bash/state-directory-manager-vamseeachanta-workspace-hub" ~/.claude/skills/majiayu000-claude-skill-registry-state-directory-manager && rm -rf "$T"
manifest:
skills/bash/state-directory-manager-vamseeachanta-workspace-hub/SKILL.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- makes HTTP requests (curl)
- references API keys
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
State Directory Manager
Patterns for managing persistent state, configuration, and cache directories in bash scripts following XDG Base Directory specification.
When to Use This Skill
✅ Use when:
- Scripts need to persist data between runs
- Storing user preferences or configuration
- Caching results for performance
- Managing log files with rotation
- Creating portable CLI tools
❌ Avoid when:
- One-time scripts that don't need state
- Scripts that should be purely stateless
- When environment variables are sufficient
Core Capabilities
1. XDG Base Directory Standard
Follow the XDG specification for directory locations:
#!/bin/bash # ABOUTME: XDG Base Directory compliant state management # ABOUTME: Cross-platform directory locations # XDG Base Directories with fallbacks XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}" XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" # Application-specific directories APP_NAME="my-tool" CONFIG_DIR="$XDG_CONFIG_HOME/$APP_NAME" DATA_DIR="$XDG_DATA_HOME/$APP_NAME" STATE_DIR="$XDG_STATE_HOME/$APP_NAME" CACHE_DIR="$XDG_CACHE_HOME/$APP_NAME" LOG_DIR="$STATE_DIR/logs" # Initialize directories init_directories() { mkdir -p "$CONFIG_DIR" mkdir -p "$DATA_DIR" mkdir -p "$STATE_DIR" mkdir -p "$CACHE_DIR" mkdir -p "$LOG_DIR" }
2. Workspace-Hub Pattern
Alternative using home directory (from workspace-hub scripts):
#!/bin/bash # ABOUTME: Workspace-hub style state directory management # ABOUTME: Simple $HOME/.app-name pattern APP_NAME="workspace-hub" APP_DIR="${HOME}/.${APP_NAME}" # Directory structure CONFIG_DIR="$APP_DIR/config" DATA_DIR="$APP_DIR/data" LOGS_DIR="$APP_DIR/logs" CACHE_DIR="$APP_DIR/cache" TEMP_DIR="$APP_DIR/tmp" # Initialize with proper permissions init_app_dirs() { local dirs=("$CONFIG_DIR" "$DATA_DIR" "$LOGS_DIR" "$CACHE_DIR" "$TEMP_DIR") for dir in "${dirs[@]}"; do if [[ ! -d "$dir" ]]; then mkdir -p "$dir" chmod 700 "$dir" # Private by default fi done } # Clean old temp files clean_temp() { find "$TEMP_DIR" -type f -mtime +1 -delete 2>/dev/null || true }
3. Configuration File Management
Read and write configuration files:
#!/bin/bash # ABOUTME: Configuration file management # ABOUTME: Key-value pairs with defaults CONFIG_FILE="$CONFIG_DIR/config" # Default configuration declare -A DEFAULT_CONFIG=( ["parallel_workers"]="5" ["log_level"]="INFO" ["auto_sync"]="true" ["timeout"]="30" ) # Initialize config with defaults init_config() { if [[ ! -f "$CONFIG_FILE" ]]; then { echo "# Configuration for $APP_NAME" echo "# Generated: $(date)" echo "" for key in "${!DEFAULT_CONFIG[@]}"; do echo "${key}=${DEFAULT_CONFIG[$key]}" done } > "$CONFIG_FILE" fi } # Read config value get_config() { local key="$1" local default="${2:-${DEFAULT_CONFIG[$key]:-}}" if [[ -f "$CONFIG_FILE" ]]; then local value value=$(grep "^${key}=" "$CONFIG_FILE" 2>/dev/null | cut -d'=' -f2-) echo "${value:-$default}" else echo "$default" fi } # Write config value set_config() { local key="$1" local value="$2" init_config if grep -q "^${key}=" "$CONFIG_FILE" 2>/dev/null; then # Update existing sed -i "s|^${key}=.*|${key}=${value}|" "$CONFIG_FILE" else # Add new echo "${key}=${value}" >> "$CONFIG_FILE" fi } # Load all config into associative array load_config() { declare -gA CONFIG # Start with defaults for key in "${!DEFAULT_CONFIG[@]}"; do CONFIG[$key]="${DEFAULT_CONFIG[$key]}" done # Override with file values if [[ -f "$CONFIG_FILE" ]]; then while IFS='=' read -r key value; do [[ "$key" =~ ^#.*$ || -z "$key" ]] && continue CONFIG[$key]="$value" done < "$CONFIG_FILE" fi } # Usage init_config load_config echo "Parallel workers: ${CONFIG[parallel_workers]}" set_config "parallel_workers" "10"
4. State File Operations
Track persistent state between runs:
#!/bin/bash # ABOUTME: State file operations # ABOUTME: Track last run, progress, etc. STATE_FILE="$STATE_DIR/state.json" # Initialize state init_state() { if [[ ! -f "$STATE_FILE" ]]; then cat > "$STATE_FILE" << EOF { "version": "1.0.0", "created": "$(date -Iseconds)", "last_run": null, "run_count": 0, "last_status": null } EOF fi } # Get state value (requires jq) get_state() { local key="$1" local default="${2:-null}" if [[ -f "$STATE_FILE" ]] && command -v jq &>/dev/null; then jq -r ".$key // $default" "$STATE_FILE" else echo "$default" fi } # Update state value (requires jq) set_state() { local key="$1" local value="$2" init_state if command -v jq &>/dev/null; then local temp=$(mktemp) jq ".$key = $value" "$STATE_FILE" > "$temp" && mv "$temp" "$STATE_FILE" fi } # Record run record_run() { local status="$1" set_state "last_run" "\"$(date -Iseconds)\"" set_state "last_status" "\"$status\"" set_state "run_count" "$(($(get_state run_count 0) + 1))" } # Simple key-value state (no jq required) STATE_KV_FILE="$STATE_DIR/state.kv" get_state_kv() { local key="$1" local default="$2" if [[ -f "$STATE_KV_FILE" ]]; then grep "^${key}=" "$STATE_KV_FILE" 2>/dev/null | cut -d'=' -f2- || echo "$default" else echo "$default" fi } set_state_kv() { local key="$1" local value="$2" mkdir -p "$(dirname "$STATE_KV_FILE")" if [[ -f "$STATE_KV_FILE" ]] && grep -q "^${key}=" "$STATE_KV_FILE"; then sed -i "s|^${key}=.*|${key}=${value}|" "$STATE_KV_FILE" else echo "${key}=${value}" >> "$STATE_KV_FILE" fi }
5. Cache Management
Implement caching with expiration:
#!/bin/bash # ABOUTME: Cache management with TTL # ABOUTME: Store and retrieve cached data CACHE_TTL="${CACHE_TTL:-3600}" # 1 hour default # Get cache file path cache_path() { local key="$1" local hash=$(echo -n "$key" | md5sum | cut -c1-16) echo "$CACHE_DIR/${hash}" } # Check if cache is valid cache_valid() { local key="$1" local ttl="${2:-$CACHE_TTL}" local path=$(cache_path "$key") if [[ -f "$path" ]]; then local age=$(($(date +%s) - $(stat -c %Y "$path" 2>/dev/null || stat -f %m "$path"))) [[ $age -lt $ttl ]] else return 1 fi } # Get from cache cache_get() { local key="$1" local ttl="${2:-$CACHE_TTL}" local path=$(cache_path "$key") if cache_valid "$key" "$ttl"; then cat "$path" return 0 fi return 1 } # Set cache cache_set() { local key="$1" local value="$2" local path=$(cache_path "$key") mkdir -p "$CACHE_DIR" echo "$value" > "$path" } # Delete cache cache_delete() { local key="$1" local path=$(cache_path "$key") rm -f "$path" } # Clear all cache cache_clear() { rm -rf "$CACHE_DIR"/* } # Clean expired cache entries cache_clean() { local ttl="${1:-$CACHE_TTL}" find "$CACHE_DIR" -type f -mmin "+$((ttl / 60))" -delete 2>/dev/null || true } # Usage with automatic caching get_with_cache() { local key="$1" local command="$2" local ttl="${3:-$CACHE_TTL}" if cache_valid "$key" "$ttl"; then cache_get "$key" else local result result=$(eval "$command") cache_set "$key" "$result" echo "$result" fi } # Example result=$(get_with_cache "api_response" "curl -s https://api.example.com/data" 300)
6. Log File Management
Manage logs with rotation:
#!/bin/bash # ABOUTME: Log file management with rotation # ABOUTME: Automatic cleanup of old logs LOG_FILE="$LOG_DIR/app.log" LOG_MAX_SIZE=$((10 * 1024 * 1024)) # 10MB LOG_MAX_FILES=5 # Initialize logging init_logging() { mkdir -p "$LOG_DIR" touch "$LOG_FILE" } # Write to log log_to_file() { local level="$1" shift local message="$*" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "[$timestamp] $level: $message" >> "$LOG_FILE" # Check if rotation needed maybe_rotate_logs } # Rotate logs if needed maybe_rotate_logs() { if [[ -f "$LOG_FILE" ]]; then local size=$(stat -c %s "$LOG_FILE" 2>/dev/null || stat -f %z "$LOG_FILE") if [[ $size -gt $LOG_MAX_SIZE ]]; then rotate_logs fi fi } # Perform log rotation rotate_logs() { # Remove oldest rm -f "${LOG_FILE}.${LOG_MAX_FILES}" # Shift existing for ((i=LOG_MAX_FILES-1; i>=1; i--)); do if [[ -f "${LOG_FILE}.$i" ]]; then mv "${LOG_FILE}.$i" "${LOG_FILE}.$((i+1))" fi done # Rotate current if [[ -f "$LOG_FILE" ]]; then mv "$LOG_FILE" "${LOG_FILE}.1" touch "$LOG_FILE" fi } # Clean old logs clean_old_logs() { local days="${1:-30}" find "$LOG_DIR" -name "*.log*" -mtime "+$days" -delete 2>/dev/null || true } # View recent logs tail_logs() { local lines="${1:-50}" tail -n "$lines" "$LOG_FILE" } # Search logs search_logs() { local pattern="$1" grep -h "$pattern" "$LOG_DIR"/*.log* 2>/dev/null | tail -100 }
Complete Example: State Manager Module
#!/bin/bash # ABOUTME: Complete state directory manager # ABOUTME: Reusable module for bash scripts # ───────────────────────────────────────────────────────────────── # State Directory Manager v1.0.0 # ───────────────────────────────────────────────────────────────── # Application identity (override in your script) : "${STATE_APP_NAME:=my-app}" # Directory setup STATE_BASE_DIR="${HOME}/.${STATE_APP_NAME}" STATE_CONFIG_DIR="$STATE_BASE_DIR/config" STATE_DATA_DIR="$STATE_BASE_DIR/data" STATE_CACHE_DIR="$STATE_BASE_DIR/cache" STATE_LOG_DIR="$STATE_BASE_DIR/logs" STATE_TMP_DIR="$STATE_BASE_DIR/tmp" # File paths STATE_CONFIG_FILE="$STATE_CONFIG_DIR/config" STATE_STATE_FILE="$STATE_DATA_DIR/state" STATE_LOG_FILE="$STATE_LOG_DIR/app.log" # Settings STATE_CACHE_TTL="${STATE_CACHE_TTL:-3600}" STATE_LOG_MAX_SIZE="${STATE_LOG_MAX_SIZE:-10485760}" STATE_LOG_MAX_FILES="${STATE_LOG_MAX_FILES:-5}" # ───────────────────────────────────────────────────────────────── # Initialization # ───────────────────────────────────────────────────────────────── state_init() { local dirs=( "$STATE_CONFIG_DIR" "$STATE_DATA_DIR" "$STATE_CACHE_DIR" "$STATE_LOG_DIR" "$STATE_TMP_DIR" ) for dir in "${dirs[@]}"; do if [[ ! -d "$dir" ]]; then mkdir -p "$dir" chmod 700 "$dir" fi done # Initialize files [[ -f "$STATE_CONFIG_FILE" ]] || touch "$STATE_CONFIG_FILE" [[ -f "$STATE_STATE_FILE" ]] || touch "$STATE_STATE_FILE" [[ -f "$STATE_LOG_FILE" ]] || touch "$STATE_LOG_FILE" } # ───────────────────────────────────────────────────────────────── # Config Functions # ───────────────────────────────────────────────────────────────── state_config_get() { local key="$1" local default="$2" grep "^${key}=" "$STATE_CONFIG_FILE" 2>/dev/null | cut -d'=' -f2- || echo "$default" } state_config_set() { local key="$1" local value="$2" if grep -q "^${key}=" "$STATE_CONFIG_FILE" 2>/dev/null; then sed -i "s|^${key}=.*|${key}=${value}|" "$STATE_CONFIG_FILE" else echo "${key}=${value}" >> "$STATE_CONFIG_FILE" fi } state_config_list() { cat "$STATE_CONFIG_FILE" 2>/dev/null | grep -v '^#' | grep -v '^$' } # ───────────────────────────────────────────────────────────────── # State Functions # ───────────────────────────────────────────────────────────────── state_get() { local key="$1" local default="$2" grep "^${key}=" "$STATE_STATE_FILE" 2>/dev/null | cut -d'=' -f2- || echo "$default" } state_set() { local key="$1" local value="$2" if grep -q "^${key}=" "$STATE_STATE_FILE" 2>/dev/null; then sed -i "s|^${key}=.*|${key}=${value}|" "$STATE_STATE_FILE" else echo "${key}=${value}" >> "$STATE_STATE_FILE" fi } # ───────────────────────────────────────────────────────────────── # Cache Functions # ───────────────────────────────────────────────────────────────── state_cache_key() { echo -n "$1" | md5sum | cut -c1-16 } state_cache_get() { local key="$1" local ttl="${2:-$STATE_CACHE_TTL}" local path="$STATE_CACHE_DIR/$(state_cache_key "$key")" if [[ -f "$path" ]]; then local age=$(($(date +%s) - $(stat -c %Y "$path" 2>/dev/null || stat -f %m "$path"))) if [[ $age -lt $ttl ]]; then cat "$path" return 0 fi fi return 1 } state_cache_set() { local key="$1" local value="$2" local path="$STATE_CACHE_DIR/$(state_cache_key "$key")" echo "$value" > "$path" } state_cache_clear() { rm -rf "$STATE_CACHE_DIR"/* } # ───────────────────────────────────────────────────────────────── # Log Functions # ───────────────────────────────────────────────────────────────── state_log() { local level="$1" shift local message="$*" echo "[$(date '+%Y-%m-%d %H:%M:%S')] $level: $message" >> "$STATE_LOG_FILE" # Auto-rotate local size=$(stat -c %s "$STATE_LOG_FILE" 2>/dev/null || echo 0) if [[ $size -gt $STATE_LOG_MAX_SIZE ]]; then state_log_rotate fi } state_log_rotate() { rm -f "${STATE_LOG_FILE}.${STATE_LOG_MAX_FILES}" for ((i=STATE_LOG_MAX_FILES-1; i>=1; i--)); do [[ -f "${STATE_LOG_FILE}.$i" ]] && mv "${STATE_LOG_FILE}.$i" "${STATE_LOG_FILE}.$((i+1))" done mv "$STATE_LOG_FILE" "${STATE_LOG_FILE}.1" touch "$STATE_LOG_FILE" } state_log_tail() { tail -n "${1:-50}" "$STATE_LOG_FILE" } # ───────────────────────────────────────────────────────────────── # Cleanup Functions # ───────────────────────────────────────────────────────────────── state_cleanup() { # Clean temp files older than 1 day find "$STATE_TMP_DIR" -type f -mtime +1 -delete 2>/dev/null || true # Clean expired cache find "$STATE_CACHE_DIR" -type f -mmin "+$((STATE_CACHE_TTL / 60))" -delete 2>/dev/null || true # Clean old logs find "$STATE_LOG_DIR" -name "*.log.*" -mtime +30 -delete 2>/dev/null || true } state_reset() { rm -rf "$STATE_BASE_DIR" state_init } # ───────────────────────────────────────────────────────────────── # Auto-initialize # ───────────────────────────────────────────────────────────────── state_init
Usage in Scripts
#!/bin/bash # Your script that uses the state manager # Set app name before sourcing STATE_APP_NAME="my-tool" # Source the state manager source /path/to/state-manager.sh # Now use it state_config_set "api_key" "abc123" api_key=$(state_config_get "api_key") state_set "last_run" "$(date -Iseconds)" state_log "INFO" "Script started" # Use cache if ! result=$(state_cache_get "api_response"); then result=$(curl -s https://api.example.com/data) state_cache_set "api_response" "$result" fi
Best Practices
- Use Standard Locations - Follow XDG or
$HOME/.app-name - Initialize Early - Call init before any operations
- Handle Permissions - Use 700 for private data
- Clean Up Regularly - Remove old temp/cache files
- Rotate Logs - Prevent unbounded growth
Resources
Version History
- 1.0.0 (2026-01-14): Initial release - extracted from workspace-hub patterns