
Plugin Settings
- 11k installs
- 139k repo stars
- Updated July 25, 2026
- anthropics/claude-code
How to store and read user-configurable settings in Claude Code plugins using .claude/plugin-name.local.md files with YAML frontmatter and markdown content.
About
Plugin Settings documents the .claude/plugin-name.local.md pattern for storing user-configurable plugin settings and state. Plugins can define YAML frontmatter for structured configuration (enabled flags, numeric limits, lists) and markdown body for prompts or instructions. Developers use this when building Claude Code plugins that need per-project customization, enabling hooks, commands, and agents to read settings via bash parsing (sed/awk), adapt behavior without restart-less hot-reload, and coordinate multi-agent workflows. Key workflows include quick-exit patterns (check file exists), parsing YAML fields (string, boolean, numeric), reading markdown body, and managing agent state across collaborative tasks.
- Store per-project settings in .claude/plugin-name.local.md with YAML frontmatter and markdown body
- Quick-exit pattern: check file existence to skip unconfigured plugins with zero overhead
- Parse YAML fields using sed/grep/awk: enabled (boolean), mode (enum), max_retries (numeric), lists
- Support agent state management and coordination: store agent names, task numbers, coordinator sessions, dependencies
- Requires Claude Code restart after changes; document in .gitignore to keep settings local and not committed
Plugin Settings by the numbers
- 10,960 all-time installs (skills.sh)
- +330 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #72 of 16,659 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
plugin settings capabilities & compatibility
- Capabilities
- store structured configuration in yaml frontmatt · store prompts and instructions in markdown body · quick exit pattern for unconfigured plugins · parse yaml fields by type (string, boolean, nume · support agent state and coordination metadata · enable hook activation/deactivation per project
- Works with
- github
- Use cases
- orchestration · planning
- Platforms
- macOS · Windows · Linux · WSL
- Runs
- Runs locally
- Pricing
- Free
What plugin settings says it does
Plugins can store user-configurable settings and state in `.claude/plugin-name.local.md` files within the project directory. This pattern uses YAML frontmatter for structured configuration and markdow
npx skills add https://github.com/anthropics/claude-code --skill plugin-settingsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11k |
|---|---|
| repo stars | ★ 139k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 25, 2026 |
| Repository | anthropics/claude-code ↗ |
What it does
Store per-project plugin configuration in YAML frontmatter files to make Claude Code plugin behavior configurable without editing code.
Who is it for?
Building Claude Code plugins that require per-project configuration, enabling/disabling hooks without editing code, storing agent state in multi-agent workflows, and coordinating agent tasks.
Skip if: Global configuration (use Claude config instead), sensitive secrets (use Claude secrets), or committed shared state.
When should I use this skill?
Designing a Claude Code plugin that needs user-configurable settings, building multi-agent coordination systems, or implementing hooks that should be toggleable per project.
What you get
Plugin developers can implement settings-driven behavior, quick-exit patterns, and agent state management; users can customize plugin behavior per project and coordinate agents via shared state files.
- .claude/plugin-name.local.md file
- YAML frontmatter schema
By the numbers
- Settings use YAML frontmatter (key: value pairs) and markdown body separated by --- markers
- Example shows multi-agent-swarm pattern with agent_name, task_number, pr_number, coordinator_session fields
- Parsing techniques cover string fields (with quote escape), boolean fields (true/false comparison), numeric fields (rang
Files
Plugin Settings Pattern for Claude Code Plugins
Overview
Plugins can store user-configurable settings and state in .claude/plugin-name.local.md files within the project directory. This pattern uses YAML frontmatter for structured configuration and markdown content for prompts or additional context.
Key characteristics:
- File location:
.claude/plugin-name.local.mdin project root - Structure: YAML frontmatter + markdown body
- Purpose: Per-project plugin configuration and state
- Usage: Read from hooks, commands, and agents
- Lifecycle: User-managed (not in git, should be in
.gitignore)
File Structure
Basic Template
---
enabled: true
setting1: value1
setting2: value2
numeric_setting: 42
list_setting: ["item1", "item2"]
---
# Additional Context
This markdown body can contain:
- Task descriptions
- Additional instructions
- Prompts to feed back to Claude
- Documentation or notesExample: Plugin State File
.claude/my-plugin.local.md:
---
enabled: true
strict_mode: false
max_retries: 3
notification_level: info
coordinator_session: team-leader
---
# Plugin Configuration
This plugin is configured for standard validation mode.
Contact @team-lead with questions.Reading Settings Files
From Hooks (Bash Scripts)
Pattern: Check existence and parse frontmatter
#!/bin/bash
set -euo pipefail
# Define state file path
STATE_FILE=".claude/my-plugin.local.md"
# Quick exit if file doesn't exist
if [[ ! -f "$STATE_FILE" ]]; then
exit 0 # Plugin not configured, skip
fi
# Parse YAML frontmatter (between --- markers)
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$STATE_FILE")
# Extract individual fields
ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//' | sed 's/^"\(.*\)"$/\1/')
STRICT_MODE=$(echo "$FRONTMATTER" | grep '^strict_mode:' | sed 's/strict_mode: *//' | sed 's/^"\(.*\)"$/\1/')
# Check if enabled
if [[ "$ENABLED" != "true" ]]; then
exit 0 # Disabled
fi
# Use configuration in hook logic
if [[ "$STRICT_MODE" == "true" ]]; then
# Apply strict validation
# ...
fiSee examples/read-settings-hook.sh for complete working example.
From Commands
Commands can read settings files to customize behavior:
---
description: Process data with plugin
allowed-tools: ["Read", "Bash"]
---
# Process Command
Steps:
1. Check if settings exist at `.claude/my-plugin.local.md`
2. Read configuration using Read tool
3. Parse YAML frontmatter to extract settings
4. Apply settings to processing logic
5. Execute with configured behaviorFrom Agents
Agents can reference settings in their instructions:
---
name: configured-agent
description: Agent that adapts to project settings
---
Check for plugin settings at `.claude/my-plugin.local.md`.
If present, parse YAML frontmatter and adapt behavior according to:
- enabled: Whether plugin is active
- mode: Processing mode (strict, standard, lenient)
- Additional configuration fieldsParsing Techniques
Extract Frontmatter
# Extract everything between --- markers
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")Read Individual Fields
String fields:
VALUE=$(echo "$FRONTMATTER" | grep '^field_name:' | sed 's/field_name: *//' | sed 's/^"\(.*\)"$/\1/')Boolean fields:
ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
# Compare: if [[ "$ENABLED" == "true" ]]; thenNumeric fields:
MAX=$(echo "$FRONTMATTER" | grep '^max_value:' | sed 's/max_value: *//')
# Use: if [[ $MAX -gt 100 ]]; thenRead Markdown Body
Extract content after second ---:
# Get everything after closing ---
BODY=$(awk '/^---$/{i++; next} i>=2' "$FILE")Common Patterns
Pattern 1: Temporarily Active Hooks
Use settings file to control hook activation:
#!/bin/bash
STATE_FILE=".claude/security-scan.local.md"
# Quick exit if not configured
if [[ ! -f "$STATE_FILE" ]]; then
exit 0
fi
# Read enabled flag
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$STATE_FILE")
ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
if [[ "$ENABLED" != "true" ]]; then
exit 0 # Disabled
fi
# Run hook logic
# ...Use case: Enable/disable hooks without editing hooks.json (requires restart).
Pattern 2: Agent State Management
Store agent-specific state and configuration:
.claude/multi-agent-swarm.local.md:
---
agent_name: auth-agent
task_number: 3.5
pr_number: 1234
coordinator_session: team-leader
enabled: true
dependencies: ["Task 3.4"]
---
# Task Assignment
Implement JWT authentication for the API.
**Success Criteria:**
- Authentication endpoints created
- Tests passing
- PR created and CI greenRead from hooks to coordinate agents:
AGENT_NAME=$(echo "$FRONTMATTER" | grep '^agent_name:' | sed 's/agent_name: *//')
COORDINATOR=$(echo "$FRONTMATTER" | grep '^coordinator_session:' | sed 's/coordinator_session: *//')
# Send notification to coordinator
tmux send-keys -t "$COORDINATOR" "Agent $AGENT_NAME completed task" EnterPattern 3: Configuration-Driven Behavior
.claude/my-plugin.local.md:
---
validation_level: strict
max_file_size: 1000000
allowed_extensions: [".js", ".ts", ".tsx"]
enable_logging: true
---
# Validation Configuration
Strict mode enabled for this project.
All writes validated against security policies.Use in hooks or commands:
LEVEL=$(echo "$FRONTMATTER" | grep '^validation_level:' | sed 's/validation_level: *//')
case "$LEVEL" in
strict)
# Apply strict validation
;;
standard)
# Apply standard validation
;;
lenient)
# Apply lenient validation
;;
esacCreating Settings Files
From Commands
Commands can create settings files:
# Setup Command
Steps:
1. Ask user for configuration preferences
2. Create `.claude/my-plugin.local.md` with YAML frontmatter
3. Set appropriate values based on user input
4. Inform user that settings are saved
5. Remind user to restart Claude Code for hooks to recognize changesTemplate Generation
Provide template in plugin README:
## Configuration
Create `.claude/my-plugin.local.md` in your project:
\`\`\`markdown
---
enabled: true
mode: standard
max_retries: 3
---
# Plugin Configuration
Your settings are active.
\`\`\`
After creating or editing, restart Claude Code for changes to take effect.Best Practices
File Naming
✅ DO:
- Use
.claude/plugin-name.local.mdformat - Match plugin name exactly
- Use
.local.mdsuffix for user-local files
❌ DON'T:
- Use different directory (not
.claude/) - Use inconsistent naming
- Use
.mdwithout.local(might be committed)
Gitignore
Always add to .gitignore:
.claude/*.local.md
.claude/*.local.jsonDocument this in plugin README.
Defaults
Provide sensible defaults when settings file doesn't exist:
if [[ ! -f "$STATE_FILE" ]]; then
# Use defaults
ENABLED=true
MODE=standard
else
# Read from file
# ...
fiValidation
Validate settings values:
MAX=$(echo "$FRONTMATTER" | grep '^max_value:' | sed 's/max_value: *//')
# Validate numeric range
if ! [[ "$MAX" =~ ^[0-9]+$ ]] || [[ $MAX -lt 1 ]] || [[ $MAX -gt 100 ]]; then
echo "⚠️ Invalid max_value in settings (must be 1-100)" >&2
MAX=10 # Use default
fiRestart Requirement
Important: Settings changes require Claude Code restart.
Document in your README:
## Changing Settings
After editing `.claude/my-plugin.local.md`:
1. Save the file
2. Exit Claude Code
3. Restart: `claude` or `cc`
4. New settings will be loadedHooks cannot be hot-swapped within a session.
Security Considerations
Sanitize User Input
When writing settings files from user input:
# Escape quotes in user input
SAFE_VALUE=$(echo "$USER_INPUT" | sed 's/"/\\"/g')
# Write to file
cat > "$STATE_FILE" <<EOF
---
user_setting: "$SAFE_VALUE"
---
EOFValidate File Paths
If settings contain file paths:
FILE_PATH=$(echo "$FRONTMATTER" | grep '^data_file:' | sed 's/data_file: *//')
# Check for path traversal
if [[ "$FILE_PATH" == *".."* ]]; then
echo "⚠️ Invalid path in settings (path traversal)" >&2
exit 2
fiPermissions
Settings files should be:
- Readable by user only (
chmod 600) - Not committed to git
- Not shared between users
Real-World Examples
multi-agent-swarm Plugin
.claude/multi-agent-swarm.local.md:
---
agent_name: auth-implementation
task_number: 3.5
pr_number: 1234
coordinator_session: team-leader
enabled: true
dependencies: ["Task 3.4"]
additional_instructions: Use JWT tokens, not sessions
---
# Task: Implement Authentication
Build JWT-based authentication for the REST API.
Coordinate with auth-agent on shared types.Hook usage (agent-stop-notification.sh):
- Checks if file exists (line 15-18: quick exit if not)
- Parses frontmatter to get coordinator_session, agent_name, enabled
- Sends notifications to coordinator if enabled
- Allows quick activation/deactivation via
enabled: true/false
ralph-wiggum Plugin
.claude/ralph-loop.local.md:
---
iteration: 1
max_iterations: 10
completion_promise: "All tests passing and build successful"
---
Fix all the linting errors in the project.
Make sure tests pass after each fix.Hook usage (stop-hook.sh):
- Checks if file exists (line 15-18: quick exit if not active)
- Reads iteration count and max_iterations
- Extracts completion_promise for loop termination
- Reads body as the prompt to feed back
- Updates iteration count on each loop
Quick Reference
File Location
project-root/
└── .claude/
└── plugin-name.local.mdFrontmatter Parsing
# Extract frontmatter
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
# Read field
VALUE=$(echo "$FRONTMATTER" | grep '^field:' | sed 's/field: *//' | sed 's/^"\(.*\)"$/\1/')Body Parsing
# Extract body (after second ---)
BODY=$(awk '/^---$/{i++; next} i>=2' "$FILE")Quick Exit Pattern
if [[ ! -f ".claude/my-plugin.local.md" ]]; then
exit 0 # Not configured
fiAdditional Resources
Reference Files
For detailed implementation patterns:
- `references/parsing-techniques.md` - Complete guide to parsing YAML frontmatter and markdown bodies
- `references/real-world-examples.md` - Deep dive into multi-agent-swarm and ralph-wiggum implementations
Example Files
Working examples in examples/:
- `read-settings-hook.sh` - Hook that reads and uses settings
- `create-settings-command.md` - Command that creates settings file
- `example-settings.md` - Template settings file
Utility Scripts
Development tools in scripts/:
- `validate-settings.sh` - Validate settings file structure
- `parse-frontmatter.sh` - Extract frontmatter fields
Implementation Workflow
To add settings to a plugin:
1. Design settings schema (which fields, types, defaults) 2. Create template file in plugin documentation 3. Add gitignore entry for .claude/*.local.md 4. Implement settings parsing in hooks/commands 5. Use quick-exit pattern (check file exists, check enabled field) 6. Document settings in plugin README with template 7. Remind users that changes require Claude Code restart
Focus on keeping settings simple and providing good defaults when settings file doesn't exist.
Create Plugin Settings
This command helps users create a .claude/my-plugin.local.md settings file.
Steps
Step 1: Ask User for Preferences
Use AskUserQuestion to gather configuration:
{
"questions": [
{
"question": "Enable plugin for this project?",
"header": "Enable Plugin",
"multiSelect": false,
"options": [
{
"label": "Yes",
"description": "Plugin will be active"
},
{
"label": "No",
"description": "Plugin will be disabled"
}
]
},
{
"question": "Validation mode?",
"header": "Mode",
"multiSelect": false,
"options": [
{
"label": "Strict",
"description": "Maximum validation and security checks"
},
{
"label": "Standard",
"description": "Balanced validation (recommended)"
},
{
"label": "Lenient",
"description": "Minimal validation only"
}
]
}
]
}Step 2: Parse Answers
Extract answers from AskUserQuestion result:
- answers["0"]: enabled (Yes/No)
- answers["1"]: mode (Strict/Standard/Lenient)
Step 3: Create Settings File
Use Write tool to create .claude/my-plugin.local.md:
---
enabled: <true if Yes, false if No>
validation_mode: <strict, standard, or lenient>
max_file_size: 1000000
notify_on_errors: true
---
# Plugin Configuration
Your plugin is configured with <mode> validation mode.
To modify settings, edit this file and restart Claude Code.Step 4: Inform User
Tell the user:
- Settings file created at
.claude/my-plugin.local.md - Current configuration summary
- How to edit manually if needed
- Reminder: Restart Claude Code for changes to take effect
- Settings file is gitignored (won't be committed)
Implementation Notes
Always validate user input before writing:
- Check mode is valid
- Validate numeric fields are numbers
- Ensure paths don't have traversal attempts
- Sanitize any free-text fields
Example Plugin Settings File
Template: Basic Configuration
.claude/my-plugin.local.md:
---
enabled: true
mode: standard
---
# My Plugin Configuration
Plugin is active in standard mode.Template: Advanced Configuration
.claude/my-plugin.local.md:
---
enabled: true
strict_mode: false
max_file_size: 1000000
allowed_extensions: [".js", ".ts", ".tsx"]
enable_logging: true
notification_level: info
retry_attempts: 3
timeout_seconds: 60
custom_path: "/path/to/data"
---
# My Plugin Advanced Configuration
This project uses custom plugin configuration with:
- Standard validation mode
- 1MB file size limit
- JavaScript/TypeScript files allowed
- Info-level logging
- 3 retry attempts
## Additional Notes
Contact @team-lead with questions about this configuration.Template: Agent State File
.claude/multi-agent-swarm.local.md:
---
agent_name: database-implementation
task_number: 4.2
pr_number: 5678
coordinator_session: team-leader
enabled: true
dependencies: ["Task 3.5", "Task 4.1"]
additional_instructions: "Use PostgreSQL, not MySQL"
---
# Task Assignment: Database Schema Implementation
Implement the database schema for the new features module.
## Requirements
- Create migration files
- Add indexes for performance
- Write tests for constraints
- Document schema in README
## Success Criteria
- Migrations run successfully
- All tests pass
- PR created with CI green
- Schema documented
## Coordination
Depends on:
- Task 3.5: API endpoint definitions
- Task 4.1: Data model design
Report status to coordinator session 'team-leader'.Template: Feature Flag Pattern
.claude/experimental-features.local.md:
---
enabled: true
features:
- ai_suggestions
- auto_formatting
- advanced_refactoring
experimental_mode: false
---
# Experimental Features Configuration
Current enabled features:
- AI-powered code suggestions
- Automatic code formatting
- Advanced refactoring tools
Experimental mode is OFF (stable features only).Usage in Hooks
These templates can be read by hooks:
# Check if plugin is configured
if [[ ! -f ".claude/my-plugin.local.md" ]]; then
exit 0 # Not configured, skip hook
fi
# Read settings
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' ".claude/my-plugin.local.md")
ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
# Apply settings
if [[ "$ENABLED" == "true" ]]; then
# Hook is active
# ...
fiGitignore
Always add to project .gitignore:
# Plugin settings (user-local, not committed)
.claude/*.local.md
.claude/*.local.jsonEditing Settings
Users can edit settings files manually:
# Edit settings
vim .claude/my-plugin.local.md
# Changes take effect after restart
exit # Exit Claude Code
claude # RestartChanges require Claude Code restart - hooks can't be hot-swapped.
#!/bin/bash
# Example hook that reads plugin settings from .claude/my-plugin.local.md
# Demonstrates the complete pattern for settings-driven hook behavior
set -euo pipefail
# Define settings file path
SETTINGS_FILE=".claude/my-plugin.local.md"
# Quick exit if settings file doesn't exist
if [[ ! -f "$SETTINGS_FILE" ]]; then
# Plugin not configured - use defaults or skip
exit 0
fi
# Parse YAML frontmatter (everything between --- markers)
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$SETTINGS_FILE")
# Extract configuration fields
ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//' | sed 's/^"\(.*\)"$/\1/')
STRICT_MODE=$(echo "$FRONTMATTER" | grep '^strict_mode:' | sed 's/strict_mode: *//' | sed 's/^"\(.*\)"$/\1/')
MAX_SIZE=$(echo "$FRONTMATTER" | grep '^max_file_size:' | sed 's/max_file_size: *//')
# Quick exit if disabled
if [[ "$ENABLED" != "true" ]]; then
exit 0
fi
# Read hook input
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
# Apply configured validation
if [[ "$STRICT_MODE" == "true" ]]; then
# Strict mode: apply all checks
if [[ "$file_path" == *".."* ]]; then
echo '{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "Path traversal blocked (strict mode)"}' >&2
exit 2
fi
if [[ "$file_path" == *".env"* ]] || [[ "$file_path" == *"secret"* ]]; then
echo '{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "Sensitive file blocked (strict mode)"}' >&2
exit 2
fi
else
# Standard mode: basic checks only
if [[ "$file_path" == "/etc/"* ]] || [[ "$file_path" == "/sys/"* ]]; then
echo '{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "System path blocked"}' >&2
exit 2
fi
fi
# Check file size if configured
if [[ -n "$MAX_SIZE" ]] && [[ "$MAX_SIZE" =~ ^[0-9]+$ ]]; then
content=$(echo "$input" | jq -r '.tool_input.content // empty')
content_size=${#content}
if [[ $content_size -gt $MAX_SIZE ]]; then
echo '{"hookSpecificOutput": {"permissionDecision": "deny"}, "systemMessage": "File exceeds configured max size: '"$MAX_SIZE"' bytes"}' >&2
exit 2
fi
fi
# All checks passed
exit 0
Settings File Parsing Techniques
Complete guide to parsing .claude/plugin-name.local.md files in bash scripts.
File Structure
Settings files use markdown with YAML frontmatter:
---
field1: value1
field2: "value with spaces"
numeric_field: 42
boolean_field: true
list_field: ["item1", "item2", "item3"]
---
# Markdown Content
This body content can be extracted separately.
It's useful for prompts, documentation, or additional context.Parsing Frontmatter
Extract Frontmatter Block
#!/bin/bash
FILE=".claude/my-plugin.local.md"
# Extract everything between --- markers (excluding the markers themselves)
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")How it works:
sed -n- Suppress automatic printing/^---$/,/^---$/- Range from first---to second---{ /^---$/d; p; }- Delete the---lines, print everything else
Extract Individual Fields
String fields:
# Simple value
VALUE=$(echo "$FRONTMATTER" | grep '^field_name:' | sed 's/field_name: *//')
# Quoted value (removes surrounding quotes)
VALUE=$(echo "$FRONTMATTER" | grep '^field_name:' | sed 's/field_name: *//' | sed 's/^"\(.*\)"$/\1/')Boolean fields:
ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
# Use in condition
if [[ "$ENABLED" == "true" ]]; then
# Enabled
fiNumeric fields:
MAX=$(echo "$FRONTMATTER" | grep '^max_value:' | sed 's/max_value: *//')
# Validate it's a number
if [[ "$MAX" =~ ^[0-9]+$ ]]; then
# Use in numeric comparison
if [[ $MAX -gt 100 ]]; then
# Too large
fi
fiList fields (simple):
# YAML: list: ["item1", "item2", "item3"]
LIST=$(echo "$FRONTMATTER" | grep '^list:' | sed 's/list: *//')
# Result: ["item1", "item2", "item3"]
# For simple checks:
if [[ "$LIST" == *"item1"* ]]; then
# List contains item1
fiList fields (proper parsing with jq):
# For proper list handling, use yq or convert to JSON
# This requires yq to be installed (brew install yq)
# Extract list as JSON array
LIST=$(echo "$FRONTMATTER" | yq -o json '.list' 2>/dev/null)
# Iterate over items
echo "$LIST" | jq -r '.[]' | while read -r item; do
echo "Processing: $item"
doneParsing Markdown Body
Extract Body Content
#!/bin/bash
FILE=".claude/my-plugin.local.md"
# Extract everything after the closing ---
# Counts --- markers: first is opening, second is closing, everything after is body
BODY=$(awk '/^---$/{i++; next} i>=2' "$FILE")How it works:
/^---$/- Match---lines{i++; next}- Increment counter and skip the---linei>=2- Print all lines after second---
Handles edge case: If --- appears in the markdown body, it still works because we only count the first two --- at the start.
Use Body as Prompt
# Extract body
PROMPT=$(awk '/^---$/{i++; next} i>=2' "$RALPH_STATE_FILE")
# Feed back to Claude
echo '{"decision": "block", "reason": "'"$PROMPT"'"}' | jq .Important: Use jq -n --arg for safer JSON construction with user content:
PROMPT=$(awk '/^---$/{i++; next} i>=2' "$FILE")
# Safe JSON construction
jq -n --arg prompt "$PROMPT" '{
"decision": "block",
"reason": $prompt
}'Common Parsing Patterns
Pattern: Field with Default
VALUE=$(echo "$FRONTMATTER" | grep '^field:' | sed 's/field: *//' | sed 's/^"\(.*\)"$/\1/')
# Use default if empty
if [[ -z "$VALUE" ]]; then
VALUE="default_value"
fiPattern: Optional Field
OPTIONAL=$(echo "$FRONTMATTER" | grep '^optional_field:' | sed 's/optional_field: *//' | sed 's/^"\(.*\)"$/\1/')
# Only use if present
if [[ -n "$OPTIONAL" ]] && [[ "$OPTIONAL" != "null" ]]; then
# Field is set, use it
echo "Optional field: $OPTIONAL"
fiPattern: Multiple Fields at Once
# Parse all fields in one pass
while IFS=': ' read -r key value; do
# Remove quotes if present
value=$(echo "$value" | sed 's/^"\(.*\)"$/\1/')
case "$key" in
enabled)
ENABLED="$value"
;;
mode)
MODE="$value"
;;
max_size)
MAX_SIZE="$value"
;;
esac
done <<< "$FRONTMATTER"Updating Settings Files
Atomic Updates
Always use temp file + atomic move to prevent corruption:
#!/bin/bash
FILE=".claude/my-plugin.local.md"
NEW_VALUE="updated_value"
# Create temp file
TEMP_FILE="${FILE}.tmp.$$"
# Update field using sed
sed "s/^field_name: .*/field_name: $NEW_VALUE/" "$FILE" > "$TEMP_FILE"
# Atomic replace
mv "$TEMP_FILE" "$FILE"Update Single Field
# Increment iteration counter
CURRENT=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//')
NEXT=$((CURRENT + 1))
# Update file
TEMP_FILE="${FILE}.tmp.$$"
sed "s/^iteration: .*/iteration: $NEXT/" "$FILE" > "$TEMP_FILE"
mv "$TEMP_FILE" "$FILE"Update Multiple Fields
# Update several fields at once
TEMP_FILE="${FILE}.tmp.$$"
sed -e "s/^iteration: .*/iteration: $NEXT_ITERATION/" \
-e "s/^pr_number: .*/pr_number: $PR_NUMBER/" \
-e "s/^status: .*/status: $NEW_STATUS/" \
"$FILE" > "$TEMP_FILE"
mv "$TEMP_FILE" "$FILE"Validation Techniques
Validate File Exists and Is Readable
FILE=".claude/my-plugin.local.md"
if [[ ! -f "$FILE" ]]; then
echo "Settings file not found" >&2
exit 1
fi
if [[ ! -r "$FILE" ]]; then
echo "Settings file not readable" >&2
exit 1
fiValidate Frontmatter Structure
# Count --- markers (should be exactly 2 at start)
MARKER_COUNT=$(grep -c '^---$' "$FILE" 2>/dev/null || echo "0")
if [[ $MARKER_COUNT -lt 2 ]]; then
echo "Invalid settings file: missing frontmatter markers" >&2
exit 1
fiValidate Field Values
MODE=$(echo "$FRONTMATTER" | grep '^mode:' | sed 's/mode: *//')
case "$MODE" in
strict|standard|lenient)
# Valid mode
;;
*)
echo "Invalid mode: $MODE (must be strict, standard, or lenient)" >&2
exit 1
;;
esacValidate Numeric Ranges
MAX_SIZE=$(echo "$FRONTMATTER" | grep '^max_size:' | sed 's/max_size: *//')
if ! [[ "$MAX_SIZE" =~ ^[0-9]+$ ]]; then
echo "max_size must be a number" >&2
exit 1
fi
if [[ $MAX_SIZE -lt 1 ]] || [[ $MAX_SIZE -gt 10000000 ]]; then
echo "max_size out of range (1-10000000)" >&2
exit 1
fiEdge Cases and Gotchas
Quotes in Values
YAML allows both quoted and unquoted strings:
# These are equivalent:
field1: value
field2: "value"
field3: 'value'Handle both:
# Remove surrounding quotes if present
VALUE=$(echo "$FRONTMATTER" | grep '^field:' | sed 's/field: *//' | sed 's/^"\(.*\)"$/\1/' | sed "s/^'\\(.*\\)'$/\\1/")--- in Markdown Body
If the markdown body contains ---, the parsing still works because we only match the first two:
---
field: value
---
# Body
Here's a separator:
---
More content after the separator.The awk '/^---$/{i++; next} i>=2' pattern handles this correctly.
Empty Values
Handle missing or empty fields:
field1:
field2: ""
field3: nullParsing:
VALUE=$(echo "$FRONTMATTER" | grep '^field1:' | sed 's/field1: *//')
# VALUE will be empty string
# Check for empty/null
if [[ -z "$VALUE" ]] || [[ "$VALUE" == "null" ]]; then
VALUE="default"
fiSpecial Characters
Values with special characters need careful handling:
message: "Error: Something went wrong!"
path: "/path/with spaces/file.txt"
regex: "^[a-zA-Z0-9_]+$"Safe parsing:
# Always quote variables when using
MESSAGE=$(echo "$FRONTMATTER" | grep '^message:' | sed 's/message: *//' | sed 's/^"\(.*\)"$/\1/')
echo "Message: $MESSAGE" # Quoted!Performance Optimization
Cache Parsed Values
If reading settings multiple times:
# Parse once
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
# Extract multiple fields from cached frontmatter
FIELD1=$(echo "$FRONTMATTER" | grep '^field1:' | sed 's/field1: *//')
FIELD2=$(echo "$FRONTMATTER" | grep '^field2:' | sed 's/field2: *//')
FIELD3=$(echo "$FRONTMATTER" | grep '^field3:' | sed 's/field3: *//')Don't: Re-parse file for each field.
Lazy Loading
Only parse settings when needed:
#!/bin/bash
input=$(cat)
# Quick checks first (no file I/O)
tool_name=$(echo "$input" | jq -r '.tool_name')
if [[ "$tool_name" != "Write" ]]; then
exit 0 # Not a write operation, skip
fi
# Only now check settings file
if [[ -f ".claude/my-plugin.local.md" ]]; then
# Parse settings
# ...
fiDebugging
Print Parsed Values
#!/bin/bash
set -x # Enable debug tracing
FILE=".claude/my-plugin.local.md"
if [[ -f "$FILE" ]]; then
echo "Settings file found" >&2
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
echo "Frontmatter:" >&2
echo "$FRONTMATTER" >&2
ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
echo "Enabled: $ENABLED" >&2
fiValidate Parsing
# Show what was parsed
echo "Parsed values:" >&2
echo " enabled: $ENABLED" >&2
echo " mode: $MODE" >&2
echo " max_size: $MAX_SIZE" >&2
# Verify expected values
if [[ "$ENABLED" != "true" ]] && [[ "$ENABLED" != "false" ]]; then
echo "⚠️ Unexpected enabled value: $ENABLED" >&2
fiAlternative: Using yq
For complex YAML, consider using yq:
# Install: brew install yq
# Parse YAML properly
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
# Extract fields with yq
ENABLED=$(echo "$FRONTMATTER" | yq '.enabled')
MODE=$(echo "$FRONTMATTER" | yq '.mode')
LIST=$(echo "$FRONTMATTER" | yq -o json '.list_field')
# Iterate list properly
echo "$LIST" | jq -r '.[]' | while read -r item; do
echo "Item: $item"
donePros:
- Proper YAML parsing
- Handles complex structures
- Better list/object support
Cons:
- Requires yq installation
- Additional dependency
- May not be available on all systems
Recommendation: Use sed/grep for simple fields, yq for complex structures.
Complete Example
#!/bin/bash
set -euo pipefail
# Configuration
SETTINGS_FILE=".claude/my-plugin.local.md"
# Quick exit if not configured
if [[ ! -f "$SETTINGS_FILE" ]]; then
# Use defaults
ENABLED=true
MODE=standard
MAX_SIZE=1000000
else
# Parse frontmatter
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$SETTINGS_FILE")
# Extract fields with defaults
ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
ENABLED=${ENABLED:-true}
MODE=$(echo "$FRONTMATTER" | grep '^mode:' | sed 's/mode: *//' | sed 's/^"\(.*\)"$/\1/')
MODE=${MODE:-standard}
MAX_SIZE=$(echo "$FRONTMATTER" | grep '^max_size:' | sed 's/max_size: *//')
MAX_SIZE=${MAX_SIZE:-1000000}
# Validate values
if [[ "$ENABLED" != "true" ]] && [[ "$ENABLED" != "false" ]]; then
echo "⚠️ Invalid enabled value, using default" >&2
ENABLED=true
fi
if ! [[ "$MAX_SIZE" =~ ^[0-9]+$ ]]; then
echo "⚠️ Invalid max_size, using default" >&2
MAX_SIZE=1000000
fi
fi
# Quick exit if disabled
if [[ "$ENABLED" != "true" ]]; then
exit 0
fi
# Use configuration
echo "Configuration loaded: mode=$MODE, max_size=$MAX_SIZE" >&2
# Apply logic based on settings
case "$MODE" in
strict)
# Strict validation
;;
standard)
# Standard validation
;;
lenient)
# Lenient validation
;;
esacThis provides robust settings handling with defaults, validation, and error recovery.
Real-World Plugin Settings Examples
Detailed analysis of how production plugins use the .claude/plugin-name.local.md pattern.
multi-agent-swarm Plugin
Settings File Structure
.claude/multi-agent-swarm.local.md:
---
agent_name: auth-implementation
task_number: 3.5
pr_number: 1234
coordinator_session: team-leader
enabled: true
dependencies: ["Task 3.4"]
additional_instructions: "Use JWT tokens, not sessions"
---
# Task: Implement Authentication
Build JWT-based authentication for the REST API.
## Requirements
- JWT token generation and validation
- Refresh token flow
- Secure password hashing
## Success Criteria
- Auth endpoints implemented
- Tests passing (100% coverage)
- PR created and CI green
- Documentation updated
## Coordination
Depends on Task 3.4 (user model).
Report status to 'team-leader' session.How It's Used
File: hooks/agent-stop-notification.sh
Purpose: Send notifications to coordinator when agent becomes idle
Implementation:
#!/bin/bash
set -euo pipefail
SWARM_STATE_FILE=".claude/multi-agent-swarm.local.md"
# Quick exit if no swarm active
if [[ ! -f "$SWARM_STATE_FILE" ]]; then
exit 0
fi
# Parse frontmatter
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$SWARM_STATE_FILE")
# Extract configuration
COORDINATOR_SESSION=$(echo "$FRONTMATTER" | grep '^coordinator_session:' | sed 's/coordinator_session: *//' | sed 's/^"\(.*\)"$/\1/')
AGENT_NAME=$(echo "$FRONTMATTER" | grep '^agent_name:' | sed 's/agent_name: *//' | sed 's/^"\(.*\)"$/\1/')
TASK_NUMBER=$(echo "$FRONTMATTER" | grep '^task_number:' | sed 's/task_number: *//' | sed 's/^"\(.*\)"$/\1/')
PR_NUMBER=$(echo "$FRONTMATTER" | grep '^pr_number:' | sed 's/pr_number: *//' | sed 's/^"\(.*\)"$/\1/')
ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//')
# Check if enabled
if [[ "$ENABLED" != "true" ]]; then
exit 0
fi
# Send notification to coordinator
NOTIFICATION="🤖 Agent ${AGENT_NAME} (Task ${TASK_NUMBER}, PR #${PR_NUMBER}) is idle."
if tmux has-session -t "$COORDINATOR_SESSION" 2>/dev/null; then
tmux send-keys -t "$COORDINATOR_SESSION" "$NOTIFICATION" Enter
sleep 0.5
tmux send-keys -t "$COORDINATOR_SESSION" Enter
fi
exit 0Key patterns: 1. Quick exit (line 7-9): Returns immediately if file doesn't exist 2. Field extraction (lines 11-17): Parses each frontmatter field 3. Enabled check (lines 19-21): Respects enabled flag 4. Action based on settings (lines 23-29): Uses coordinator_session to send notification
Creation
File: commands/launch-swarm.md
Settings files are created during swarm launch with:
cat > "$WORKTREE_PATH/.claude/multi-agent-swarm.local.md" <<EOF
---
agent_name: $AGENT_NAME
task_number: $TASK_ID
pr_number: TBD
coordinator_session: $COORDINATOR_SESSION
enabled: true
dependencies: [$DEPENDENCIES]
additional_instructions: "$EXTRA_INSTRUCTIONS"
---
# Task: $TASK_DESCRIPTION
$TASK_DETAILS
EOFUpdates
PR number updated after PR creation:
# Update pr_number field
sed "s/^pr_number: .*/pr_number: $PR_NUM/" \
".claude/multi-agent-swarm.local.md" > temp.md
mv temp.md ".claude/multi-agent-swarm.local.md"ralph-wiggum Plugin
Settings File Structure
.claude/ralph-loop.local.md:
---
iteration: 1
max_iterations: 10
completion_promise: "All tests passing and build successful"
started_at: "2025-01-15T14:30:00Z"
---
Fix all the linting errors in the project.
Make sure tests pass after each fix.
Document any changes needed in CLAUDE.md.How It's Used
File: hooks/stop-hook.sh
Purpose: Prevent session exit and loop Claude's output back as input
Implementation:
#!/bin/bash
set -euo pipefail
RALPH_STATE_FILE=".claude/ralph-loop.local.md"
# Quick exit if no active loop
if [[ ! -f "$RALPH_STATE_FILE" ]]; then
exit 0
fi
# Parse frontmatter
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$RALPH_STATE_FILE")
# Extract configuration
ITERATION=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//')
MAX_ITERATIONS=$(echo "$FRONTMATTER" | grep '^max_iterations:' | sed 's/max_iterations: *//')
COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/')
# Check max iterations
if [[ $MAX_ITERATIONS -gt 0 ]] && [[ $ITERATION -ge $MAX_ITERATIONS ]]; then
echo "🛑 Ralph loop: Max iterations ($MAX_ITERATIONS) reached."
rm "$RALPH_STATE_FILE"
exit 0
fi
# Get transcript and check for completion promise
TRANSCRIPT_PATH=$(echo "$HOOK_INPUT" | jq -r '.transcript_path')
LAST_OUTPUT=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" | tail -1 | jq -r '.message.content | map(select(.type == "text")) | map(.text) | join("\n")')
# Check for completion
if [[ "$COMPLETION_PROMISE" != "null" ]] && [[ -n "$COMPLETION_PROMISE" ]]; then
PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -pe 's/.*?<promise>(.*?)<\/promise>.*/$1/s; s/^\s+|\s+$//g')
if [[ "$PROMISE_TEXT" = "$COMPLETION_PROMISE" ]]; then
echo "✅ Ralph loop: Detected completion"
rm "$RALPH_STATE_FILE"
exit 0
fi
fi
# Continue loop - increment iteration
NEXT_ITERATION=$((ITERATION + 1))
# Extract prompt from markdown body
PROMPT_TEXT=$(awk '/^---$/{i++; next} i>=2' "$RALPH_STATE_FILE")
# Update iteration counter
TEMP_FILE="${RALPH_STATE_FILE}.tmp.$$"
sed "s/^iteration: .*/iteration: $NEXT_ITERATION/" "$RALPH_STATE_FILE" > "$TEMP_FILE"
mv "$TEMP_FILE" "$RALPH_STATE_FILE"
# Block exit and feed prompt back
jq -n \
--arg prompt "$PROMPT_TEXT" \
--arg msg "🔄 Ralph iteration $NEXT_ITERATION" \
'{
"decision": "block",
"reason": $prompt,
"systemMessage": $msg
}'
exit 0Key patterns: 1. Quick exit (line 7-9): Skip if not active 2. Iteration tracking (lines 11-20): Count and enforce max iterations 3. Promise detection (lines 25-33): Check for completion signal in output 4. Prompt extraction (line 38): Read markdown body as next prompt 5. State update (lines 40-43): Increment iteration atomically 6. Loop continuation (lines 45-53): Block exit and feed prompt back
Creation
File: scripts/setup-ralph-loop.sh
#!/bin/bash
PROMPT="$1"
MAX_ITERATIONS="${2:-0}"
COMPLETION_PROMISE="${3:-}"
# Create state file
cat > ".claude/ralph-loop.local.md" <<EOF
---
iteration: 1
max_iterations: $MAX_ITERATIONS
completion_promise: "$COMPLETION_PROMISE"
started_at: "$(date -Iseconds)"
---
$PROMPT
EOF
echo "Ralph loop initialized: .claude/ralph-loop.local.md"Pattern Comparison
| Feature | multi-agent-swarm | ralph-wiggum |
|---|---|---|
| File | .claude/multi-agent-swarm.local.md | .claude/ralph-loop.local.md |
| Purpose | Agent coordination state | Loop iteration state |
| Frontmatter | Agent metadata | Loop configuration |
| Body | Task assignment | Prompt to loop |
| Updates | PR number, status | Iteration counter |
| Deletion | Manual or on completion | On loop exit |
| Hook | Stop (notifications) | Stop (loop control) |
Best Practices from Real Plugins
1. Quick Exit Pattern
Both plugins check file existence first:
if [[ ! -f "$STATE_FILE" ]]; then
exit 0 # Not active
fiWhy: Avoids errors when plugin isn't configured and performs fast.
2. Enabled Flag
Both use an enabled field for explicit control:
enabled: trueWhy: Allows temporary deactivation without deleting file.
3. Atomic Updates
Both use temp file + atomic move:
TEMP_FILE="${FILE}.tmp.$$"
sed "s/^field: .*/field: $NEW_VALUE/" "$FILE" > "$TEMP_FILE"
mv "$TEMP_FILE" "$FILE"Why: Prevents corruption if process is interrupted.
4. Quote Handling
Both strip surrounding quotes from YAML values:
sed 's/^"\(.*\)"$/\1/'Why: YAML allows both field: value and field: "value".
5. Error Handling
Both handle missing/corrupt files gracefully:
if [[ ! -f "$FILE" ]]; then
exit 0 # No error, just not configured
fi
if [[ -z "$CRITICAL_FIELD" ]]; then
echo "Settings file corrupt" >&2
rm "$FILE" # Clean up
exit 0
fiWhy: Fails gracefully instead of crashing.
Anti-Patterns to Avoid
❌ Hardcoded Paths
# BAD
FILE="/Users/alice/.claude/my-plugin.local.md"
# GOOD
FILE=".claude/my-plugin.local.md"❌ Unquoted Variables
# BAD
echo $VALUE
# GOOD
echo "$VALUE"❌ Non-Atomic Updates
# BAD: Can corrupt file if interrupted
sed -i "s/field: .*/field: $VALUE/" "$FILE"
# GOOD: Atomic
TEMP_FILE="${FILE}.tmp.$$"
sed "s/field: .*/field: $VALUE/" "$FILE" > "$TEMP_FILE"
mv "$TEMP_FILE" "$FILE"❌ No Default Values
# BAD: Fails if field missing
if [[ $MAX -gt 100 ]]; then
# MAX might be empty!
fi
# GOOD: Provide default
MAX=${MAX:-10}❌ Ignoring Edge Cases
# BAD: Assumes exactly 2 --- markers
sed -n '/^---$/,/^---$/{ /^---$/d; p; }'
# GOOD: Handles --- in body
awk '/^---$/{i++; next} i>=2' # For bodyConclusion
The .claude/plugin-name.local.md pattern provides:
- Simple, human-readable configuration
- Version-control friendly (gitignored)
- Per-project settings
- Easy parsing with standard bash tools
- Supports both structured config (YAML) and freeform content (markdown)
Use this pattern for any plugin that needs user-configurable behavior or state persistence.
#!/bin/bash
# Frontmatter Parser Utility
# Extracts YAML frontmatter from .local.md files
set -euo pipefail
# Usage
show_usage() {
echo "Usage: $0 <settings-file.md> [field-name]"
echo ""
echo "Examples:"
echo " # Show all frontmatter"
echo " $0 .claude/my-plugin.local.md"
echo ""
echo " # Extract specific field"
echo " $0 .claude/my-plugin.local.md enabled"
echo ""
echo " # Extract and use in script"
echo " ENABLED=\$($0 .claude/my-plugin.local.md enabled)"
exit 0
}
if [ $# -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
show_usage
fi
FILE="$1"
FIELD="${2:-}"
# Validate file
if [ ! -f "$FILE" ]; then
echo "Error: File not found: $FILE" >&2
exit 1
fi
# Extract frontmatter
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$FILE")
if [ -z "$FRONTMATTER" ]; then
echo "Error: No frontmatter found in $FILE" >&2
exit 1
fi
# If no field specified, output all frontmatter
if [ -z "$FIELD" ]; then
echo "$FRONTMATTER"
exit 0
fi
# Extract specific field
VALUE=$(echo "$FRONTMATTER" | grep "^${FIELD}:" | sed "s/${FIELD}: *//" | sed 's/^"\(.*\)"$/\1/' | sed "s/^'\\(.*\\)'$/\\1/")
if [ -z "$VALUE" ]; then
echo "Error: Field '$FIELD' not found in frontmatter" >&2
exit 1
fi
echo "$VALUE"
exit 0
#!/bin/bash
# Settings File Validator
# Validates .claude/plugin-name.local.md structure
set -euo pipefail
# Usage
if [ $# -eq 0 ]; then
echo "Usage: $0 <path/to/settings.local.md>"
echo ""
echo "Validates plugin settings file for:"
echo " - File existence and readability"
echo " - YAML frontmatter structure"
echo " - Required --- markers"
echo " - Field format"
echo ""
echo "Example: $0 .claude/my-plugin.local.md"
exit 1
fi
SETTINGS_FILE="$1"
echo "🔍 Validating settings file: $SETTINGS_FILE"
echo ""
# Check 1: File exists
if [ ! -f "$SETTINGS_FILE" ]; then
echo "❌ File not found: $SETTINGS_FILE"
exit 1
fi
echo "✅ File exists"
# Check 2: File is readable
if [ ! -r "$SETTINGS_FILE" ]; then
echo "❌ File is not readable"
exit 1
fi
echo "✅ File is readable"
# Check 3: Has frontmatter markers
MARKER_COUNT=$(grep -c '^---$' "$SETTINGS_FILE" 2>/dev/null || echo "0")
if [ "$MARKER_COUNT" -lt 2 ]; then
echo "❌ Invalid frontmatter: found $MARKER_COUNT '---' markers (need at least 2)"
echo " Expected format:"
echo " ---"
echo " field: value"
echo " ---"
echo " Content..."
exit 1
fi
echo "✅ Frontmatter markers present"
# Check 4: Extract and validate frontmatter
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$SETTINGS_FILE")
if [ -z "$FRONTMATTER" ]; then
echo "❌ Empty frontmatter (nothing between --- markers)"
exit 1
fi
echo "✅ Frontmatter not empty"
# Check 5: Frontmatter has valid YAML-like structure
if ! echo "$FRONTMATTER" | grep -q ':'; then
echo "⚠️ Warning: Frontmatter has no key:value pairs"
fi
# Check 6: Look for common fields
echo ""
echo "Detected fields:"
echo "$FRONTMATTER" | grep '^[a-z_][a-z0-9_]*:' | while IFS=':' read -r key value; do
echo " - $key: ${value:0:50}"
done
# Check 7: Validate common boolean fields
for field in enabled strict_mode; do
VALUE=$(echo "$FRONTMATTER" | grep "^${field}:" | sed "s/${field}: *//" || true)
if [ -n "$VALUE" ]; then
if [ "$VALUE" != "true" ] && [ "$VALUE" != "false" ]; then
echo "⚠️ Field '$field' should be boolean (true/false), got: $VALUE"
fi
fi
done
# Check 8: Check body exists
BODY=$(awk '/^---$/{i++; next} i>=2' "$SETTINGS_FILE")
echo ""
if [ -n "$BODY" ]; then
BODY_LINES=$(echo "$BODY" | wc -l | tr -d ' ')
echo "✅ Markdown body present ($BODY_LINES lines)"
else
echo "⚠️ No markdown body (frontmatter only)"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Settings file structure is valid"
echo ""
echo "Reminder: Changes to this file require restarting Claude Code"
exit 0
Related skills
How it compares
Use plugin settings for Claude Code plugin config files; use application env vars when building standalone services outside the Claude plugin ecosystem.
FAQ
Where should plugin settings files be stored?
.claude/plugin-name.local.md in the project root. File location is always .claude/ directory, and filename matches the plugin name with .local.md suffix.
How do I read YAML fields from settings files in bash?
Use sed to extract frontmatter between --- markers, then grep and sed to parse individual fields. Example: ENABLED=$(echo "$FRONTMATTER" | grep '^enabled:' | sed 's/enabled: *//') for boolean fields.
Do settings changes take effect immediately or require restart?
Claude Code restart is required. Hooks cannot be hot-swapped within a session. Document this requirement in plugin README and remind users to exit and restart after editing .local.md files.
Is Plugin Settings safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.