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:

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:

 1---
 2name: bash-scripting
 3description: Guideline for writing bash scripts. Use whenever asked to write a bash script.
 4---
 5
 6# Write a bash script for the given task
 7
 8## Instructions
 9
10Write a bash script for $ARGUMENTS. Make sure to:
11- Keep it simple, readable and apply clean code principles.
12- Follow UNIX principles.
13- Make it easy to use.
14- Write a concise usage summary and add a '--help' flag.
15
16Reference the following sources for writing CLI tools:
17- http://clig.dev/
18
19Reference this documentation for writing bash scripts:
20- https://tldp.org/LDP/abs/html/
21
22After you finished writing the script, check it with:
23shellcheck -s bash -C auto <path/to/script>
24Analyze 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:

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:

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.

1#!/usr/bin/env bash
2set -euo pipefail

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 http://clig.dev/ and https://tldp.org/LDP/abs/html/. 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:

  1---
  2name: bash-scripting
  3description: Guidelines for writing production-quality bash scripts. Use whenever asked to write a bash script.
  4---
  5
  6<role>
  7You are an expert bash developer who writes clean, portable, and robust shell scripts.
  8</role>
  9
 10<cli_design_principles>
 11## Human-Friendly Design
 12- Provide `--help` and `-h` flags; show usage on invalid input
 13- Use full words for long flags (`--output` not `--outp`)
 14- Confirm destructive actions unless `--force` is passed
 15- Show progress for long operations; support `--quiet` and `--verbose`
 16
 17## Composability
 18- Write output to stdout by default; support `--output <file>`
 19- Write errors and diagnostics to stderr only
 20- Accept stdin when `-` is given as filename
 21- Produce one record per line for text output (pipeline-friendly)
 22- Offer `--json` for machine-parseable output when appropriate
 23
 24## Arguments & Flags
 25- Required values → positional arguments
 26- Optional values → flags with sensible defaults
 27- Support `--` to separate flags from positional arguments
 28- Provide short flags for common operations (`-v`, `-h`, `-o`)
 29
 30## Exit Codes
 31- `0` = success
 32- `1` = general error  
 33- `2` = invalid usage
 34
 35## Error Messages
 36- Format: `scriptname: error: what went wrong`
 37- Include what happened and how to fix it
 38- Suggest `--help` on invalid usage
 39
 40## Robustness
 41- Validate arguments before doing any work
 42- Fail fast—don't partially complete then error
 43- Clean up temp files on exit (use trap)
 44
 45</cli_design_principles>
 46
 47Be idempotent where possible (safe to run twice)
 48
 49<bash_coding_standards>
 50## Script Header
 51Always start with:
 52```bash
 53#!/usr/bin/env bash
 54set -euo pipefail
 55```
 56
 57## Variables
 58- Lowercase for local: `local filename`
 59- UPPERCASE for exported/constants: `readonly VERSION="1.0.0"`
 60- Always quote: `"$var"` not `$var`
 61- Defaults: `${var:-default}`, required: `${var:?error message}`
 62
 63## Conditionals
 64- Use `[[ ]]` not `[ ]`
 65- Use `(( ))` for arithmetic
 66- Check command existence: `command -v git &>/dev/null`
 67
 68## Functions
 69- Use `local` for all variables
 70- Keep functions short and single-purpose
 71- Return status with `return`, output with `echo`
 72
 73## Error Handling & Cleanup
 74```bash
 75cleanup() { rm -f "$tmpfile"; }
 76trap cleanup EXIT
 77
 78die() { echo "${0##*/}: error: $*" >&2; exit 1; }
 79```
 80
 81## Argument Parsing Pattern
 82```bash
 83while [[ $# -gt 0 ]]; do
 84    case "$1" in
 85        -h|--help) usage; exit 0 ;;
 86        -o|--output) output="$2"; shift 2 ;;
 87        --) shift; break ;;
 88        -*) die "unknown option: $1" ;;
 89        *) break ;;
 90    esac
 91done
 92```
 93
 94## Safe Iteration
 95```bash
 96# Over lines (handles whitespace)
 97while IFS= read -r line; do ...; done < "$file"
 98
 99# Over globs (handles missing matches)
100for f in *.txt; do [[ -e "$f" ]] || continue; ...; done
101```
102</bash_coding_standards>
103
104<template>
105Use this structure as a starting point:
106
107```bash
108#!/usr/bin/env bash
109set -euo pipefail
110
111readonly SCRIPT_NAME="${0##*/}"
112readonly VERSION="1.0.0"
113
114usage() {
115    cat >&2 <<EOF
116Usage: $SCRIPT_NAME [OPTIONS] <required_arg>
117
118Brief description of what this script does.
119
120Arguments:
121    required_arg    Description of required argument
122
123Options:
124    -h, --help      Show this help message
125    -v, --verbose   Enable verbose output
126    -o, --output    Output file (default: stdout)
127
128Examples:
129    $SCRIPT_NAME input.txt
130    $SCRIPT_NAME -o result.txt input.txt
131    echo "data" | $SCRIPT_NAME -
132EOF
133}
134
135die() {
136    echo "$SCRIPT_NAME: error: $*" >&2
137    exit 1
138}
139
140main() {
141    local verbose=false
142    local output="/dev/stdout"
143
144    while [[ $# -gt 0 ]]; do
145        case "$1" in
146            -h|--help) usage; exit 0 ;;
147            -v|--verbose) verbose=true; shift ;;
148            -o|--output) output="$2"; shift 2 ;;
149            --) shift; break ;;
150            -*) die "unknown option: $1 (see --help)" ;;
151            *) break ;;
152        esac
153    done
154
155    [[ $# -ge 1 ]] || { usage; exit 2; }
156
157    local input="$1"
158
159    # Check dependencies
160    # command -v jq &>/dev/null || die "jq is required but not installed"
161
162    # Main logic here
163}
164
165main "$@"
166```
167</template>
168
169<output_format>
170Provide:
1711. The complete bash script
1722. 2-3 sentences explaining key design decisions
1733. Example commands showing typical usage
174</output_format>
175
176<validation>
177Remind the user to validate with:
178```bash
179shellcheck -s bash script.sh
180```
181</validation>

Lessons learned: