Building better bash scripts with Claude Code skills
I often find myself writing scripts to automate small repetitive CLI tasks. The last one that I wrote was a script that creates a docker container and pushes it to Amazon ECR. Afterwards it upgrades a Helm chart with that new container image in a k8s cluster. Running every command becomes annoying after doing it a few times.
Nowadays I don’t write the scripts by hand anymore, I just ask an AI to write them form me really quick. Honestly, for small scripts like these, I don’t think anyone should waste time writing them by hand anymore. However, I do like that these scripts follow the best practices of writing bash scripts and CLI tools. I’m checking these two websites from time to time when I write scripts:
- https://clig.dev/
- https://tldp.org/LDP/abs/html/ (I don’t really check this one by hand but I land on it after clicking a link somewhere).
Claude Code is what the AI that I use the most for software engineering tasks. It has this cool feature called skills that allows you to implement custom behaviour in prompts. This gave me the idea of writing a skill that helps me in implementing bash scripts that follow best practices. So I launched my editor and wrote the following SKILL.md template after skimming through the Claude Code Docs:
---
name: bash-scripting
description: Guideline for writing bash scripts. Use whenever asked to write a bash script.
---
# Write a bash script for the given task
## Instructions
Write a bash script for $ARGUMENTS. Make sure to:
- Keep it simple, readable and apply clean code principles.
- Follow UNIX principles.
- Make it easy to use.
- Write a concise usage summary and add a '--help' flag.
Reference the following sources for writing CLI tools:
- http://clig.dev/
Reference this documentation for writing bash scripts:
- https://tldp.org/LDP/abs/html/
After you finished writing the script, check it with:
shellcheck -s bash -C auto <path/to/script>
Analyze the output of the command and fix the issues reported by shellcheck if any. It is not at all complete, but I wanted to use Claude to refine it. So I wrote a prompt where I copied the text above and asked Claude to help me improve this SKILL.md.
What was good about this was:
- Clear purpose and trigger condition
- Good external references
- Validation step with
shellcheck
However, in this form, it is not even close to a good skill.
Mistake #1 (and the biggest): the instructions are vague.
The instructions must be specific, i.e. instead of saying ‘Follow UNIX principles’ you simply list the principles themselves, such as:
- Write output to stdout by default; support
--output <file> - Write errors and diagnostics to stderr only
- Accept stdin when
-is given as filename - Produce one record per line for text output (pipeline-friendly)
- Offer
--jsonfor machine-parseable output when appropriate
Instead of saying ‘follow bash scripting best practices’, write them out. Showing Claude what good looks like, using actual code, is more effective than describing it.
- Always start the script with:
#!/usr/bin/env bash
set -euo pipefail - Always quote:
"$var"not$var - Defaults:
${var:-default}, required:${var:?error message}
Mistake #2: the skill is not self-contained
Skills have to be self-contained, they should not depend on external references such as links. Unless web search is enabled and explicitly used, Claude cannot browse URLs and will rely on its training data (which might not be up-to-date).
One more reason to avoid links is for context efficiency. Getting a whole website page (or dumping a whole doc) will fill up the context with unnecessary data. Prefer a shorter summary instead with the most essential principles. Skills are meant to reliably shape behaviour, a short summary is enough to guide Claude to output a better response.
Mistake #3: the skill uses $ARGUMENTS
Be clear about the purpose of the skill and the trigger condition in the skill description, and drop the $ARGUMENTS. $ARGUMENTS is only populated when you explicitly invoke the skill with a slash command, e.g. /bash-scripting .... If the skill is triggered automatically based on the description field, the arguments will be missing and the task will be empty: Write a bash script for . Make sure to:. This is confusing and wastes tokens.
Claude can infer what needs to be done from the prompt and will select the skill to shape the response based on the description, so you don’t need $ARGUMENTS.
I asked Claude to analyze, extract and summarize the most pragmatic ideas from the links above. I’ve added them to the skill. Also, I used XML tags to delimit sections instead of using only Markdown headers (Claude also gave me this tip). After a couple more iterations, this is the final SKILL.md:
---
name: bash-scripting
description: Guidelines for writing production-quality bash scripts. Use whenever asked to write a bash script.
---
<role>
You are an expert bash developer who writes clean, portable, and robust shell scripts.
</role>
<cli_design_principles>
## Human-Friendly Design
- Provide `--help` and `-h` flags; show usage on invalid input
- Use full words for long flags (`--output` not `--outp`)
- Confirm destructive actions unless `--force` is passed
- Show progress for long operations; support `--quiet` and `--verbose`
## Composability
- Write output to stdout by default; support `--output <file>`
- Write errors and diagnostics to stderr only
- Accept stdin when `-` is given as filename
- Produce one record per line for text output (pipeline-friendly)
- Offer `--json` for machine-parseable output when appropriate
## Arguments & Flags
- Required values → positional arguments
- Optional values → flags with sensible defaults
- Support `--` to separate flags from positional arguments
- Provide short flags for common operations (`-v`, `-h`, `-o`)
## Exit Codes
- `0` = success
- `1` = general error
- `2` = invalid usage
## Error Messages
- Format: `scriptname: error: what went wrong`
- Include what happened and how to fix it
- Suggest `--help` on invalid usage
## Robustness
- Validate arguments before doing any work
- Fail fast—don't partially complete then error
- Clean up temp files on exit (use trap)
</cli_design_principles>
Be idempotent where possible (safe to run twice)
<bash_coding_standards>
## Script Header
Always start with:
```bash
#!/usr/bin/env bash
set -euo pipefail
```
## Variables
- Lowercase for local: `local filename`
- UPPERCASE for exported/constants: `readonly VERSION="1.0.0"`
- Always quote: `"$var"` not `$var`
- Defaults: `${var:-default}`, required: `${var:?error message}`
## Conditionals
- Use `[[ ]]` not `[ ]`
- Use `(( ))` for arithmetic
- Check command existence: `command -v git &>/dev/null`
## Functions
- Use `local` for all variables
- Keep functions short and single-purpose
- Return status with `return`, output with `echo`
## Error Handling & Cleanup
```bash
cleanup() { rm -f "$tmpfile"; }
trap cleanup EXIT
die() { echo "${0##*/}: error: $*" >&2; exit 1; }
```
## Argument Parsing Pattern
```bash
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-o|--output) output="$2"; shift 2 ;;
--) shift; break ;;
-*) die "unknown option: $1" ;;
*) break ;;
esac
done
```
## Safe Iteration
```bash
# Over lines (handles whitespace)
while IFS= read -r line; do ...; done < "$file"
# Over globs (handles missing matches)
for f in *.txt; do [[ -e "$f" ]] || continue; ...; done
```
</bash_coding_standards>
<template>
Use this structure as a starting point:
```bash
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_NAME="${0##*/}"
readonly VERSION="1.0.0"
usage() {
cat >&2 <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <required_arg>
Brief description of what this script does.
Arguments:
required_arg Description of required argument
Options:
-h, --help Show this help message
-v, --verbose Enable verbose output
-o, --output Output file (default: stdout)
Examples:
$SCRIPT_NAME input.txt
$SCRIPT_NAME -o result.txt input.txt
echo "data" | $SCRIPT_NAME -
EOF
}
die() {
echo "$SCRIPT_NAME: error: $*" >&2
exit 1
}
main() {
local verbose=false
local output="/dev/stdout"
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage; exit 0 ;;
-v|--verbose) verbose=true; shift ;;
-o|--output) output="$2"; shift 2 ;;
--) shift; break ;;
-*) die "unknown option: $1 (see --help)" ;;
*) break ;;
esac
done
[[ $# -ge 1 ]] || { usage; exit 2; }
local input="$1"
# Check dependencies
# command -v jq &>/dev/null || die "jq is required but not installed"
# Main logic here
}
main "$@"
```
</template>
<output_format>
Provide:
1. The complete bash script
2. 2-3 sentences explaining key design decisions
3. Example commands showing typical usage
</output_format>
<validation>
Remind the user to validate with:
```bash
shellcheck -s bash script.sh
```
</validation> Lessons learned:
- You need to be really specific about the instructions. Do not say ‘follow best principles’, list the principles themselves instead.
- Showing Claude what good looks like is more effective than describing it.
- Do not provide links to websites in the
SKILL.md. Skills have to be self-contained. Summarize the ideas from those websites and add them to the SKILL.md. - It’s up to you to prioritise what’s most important for the skill to avoid filling up the context. You know what matters.
- Claude responds better to sections that are delimited by XML tags rather than Markdown headers.
- Be clear about the purpose of the skill and the trigger condition in the skill description, and drop the $ARGUMENTS.