- Tue 07 July 2026
- 7 min read
- Cooking
- #cooking, #recipe, #bash, #off-topic
Table of Contents
Every household has one dish that started as an accident and ended up in production. Ours arrived some years ago in a HelloFresh box - yes, the meal-kit subscription, our guilty pleasure phase - as an unassuming chickpea curry. We cooked it once, looked at each other, and knew this thing was not going back into the backlog. The subscription is long gone; the recipe survived the migration. It has been running in our kitchen on a roughly weekly release cadence ever since, patched and tuned along the way like any long-lived service: the spice list grew, the onion-garlic-ginger paste became a hard dependency, and whole spices got promoted from experiment to optional-but-recommended.

At some point I did what I do with everything that runs regularly and must not fail: I wrote it down as a script. Not as a joke, or at least not only as a joke. The shell script format turns out to be a genuinely good fit for recipes. It forces you to declare your dependencies up front, it makes the sequencing explicit, and set -euo pipefail is exactly the attitude you want at the stove: if a step fails, stop and look at it, do not keep stirring and hope.
So here it is. First the source code, then the runbook for humans who would rather not parse bash while hungry.
The Source
#!/usr/bin/env bash
set -euo pipefail
# ============================================================
# chana_masala.sh
# Version: 1.0.0
# Description: Chickpea masala for hungry IT nerds
# Runtime: approx. 25-30 minutes
# Dependencies: pan, stove, appetite
# ============================================================
PAN="/dev/pan"
STOVE="/sys/kitchen/stove"
PLATE="/home/user/plate"
OIL="oil or ghee"
CHICKPEAS="pre-cooked chickpeas"
TOMATOES="fresh tomatoes or a can of chopped tomatoes"
BASE_PASTE="onion-garlic-ginger paste"
TOPPING="cilantro || parsley || true"
SPICES=(
"ground cumin"
"ground coriander"
"paprika"
"chili powder"
"garam masala"
"turmeric"
"salt"
"pepper"
"bay leaf"
)
OPTIONAL_WHOLE_SPICES=(
"whole cumin seeds"
"cinnamon stick"
"green cardamom pods"
"coriander seeds"
)
function prepare_paste() {
echo "[1/4] Initializing aroma base..."
dice "$TOMATOES" > /tmp/tomatoes.cubes
peel "onions" "garlic" "ginger" \
| foodprocessor --mode=paste \
> /tmp/ogg_paste.masala
# Fallback for systems without a food processor:
# knife --fine-chop onions garlic ginger > /tmp/ogg_paste.masala
echo "Onion-garlic-ginger paste built successfully."
}
function build_masala() {
echo "[2/4] Starting masala engine..."
heat "$PAN" --level=high
cat "$OIL" > "$PAN"
for spice in "${OPTIONAL_WHOLE_SPICES[@]}"; do
echo "$spice" >> "$PAN"
done
sleep 15 # seconds: launch the aroma bootloader
cat /tmp/ogg_paste.masala >> "$PAN"
heat "$PAN" --level=medium
sleep 180 # sweat gently, do not let it commit (burn)
for spice in "${SPICES[@]}"; do
echo "$spice" >> "$PAN"
done
sleep 35 # compile spices and release aroma
cat /tmp/tomatoes.cubes >> "$PAN"
fry "$PAN" --mode=sharp
simmer "$PAN" --until="tomatoes soft"
echo "water" >> "$PAN"
simmer "$PAN" --mode=sauce
}
function add_chickpeas() {
echo "[3/4] Deploying chickpeas..."
cat "$CHICKPEAS" >> "$PAN"
if sauce --is-too-thick "$PAN"; then
echo "a splash of water" >> "$PAN"
fi
taste "$PAN" \
| adjust --with="salt,pepper"
lid --close "$PAN"
heat "$PAN" --level=low
sleep 300 # 5 minutes gentle simmer
}
function serve() {
echo "[4/4] Plating the release candidate..."
cp "$PAN" "$PLATE"
echo "fresh cilantro" >> "$PLATE"
echo "sesame seeds" >> "$PLATE"
echo "garlic topping" >> "$PLATE"
serve_with --any-of "basmati rice" "roti" "chapati" "naan"
sidecar --optional "yogurt-mint dip"
echo "Status: 200 OK - dinner served successfully."
}
function main() {
require appetite
require pan
require stove
prepare_paste
build_masala
add_chickpeas
serve
echo "Done. Do not deploy to production without seconds."
}
main "$@"
The Runbook
The script is complete, but no on-call engineer should have to reverse-engineer dinner from bash. Here is the same procedure in prose, with the default values we converged on after years of operation. Serves two to three, depending on appetite and whether anyone honors the seconds warning at the end.
Dependencies
- 2 cans of chickpeas (about 480 g drained), or the equivalent pre-cooked from dry
- 400 g chopped tomatoes, fresh or from a can - the can is fine, this is a weeknight service, not a tasting menu
- 2 onions, 4 cloves of garlic, a thumb-sized piece of ginger
- 2 tbsp oil or ghee
- Ground spices: 1 tsp cumin, 1 tsp coriander, 1 tsp paprika, ½ tsp chili powder (tune to your threat model), 1 tsp garam masala, ½ tsp turmeric, 1 bay leaf, salt and pepper
- Optional whole spices: ½ tsp cumin seeds, 1 cinnamon stick, 2-3 green cardamom pods, ½ tsp coriander seeds
- Topping: fresh cilantro (or parsley, if cilantro tastes like soap on your hardware - that is a genetic compile flag, not a character flaw), sesame seeds, crispy garlic
- Sidecar: basmati rice, roti, chapati, or naan; optionally a yogurt-mint dip
prepare_paste
Dice the tomatoes and set them aside. Peel the onions, garlic, and ginger and run them through a food processor until they form a rough paste. No food processor? The documented fallback applies: chop everything as finely as your knife skills allow. The paste is the load-bearing component of this dish; do not skip it in favor of lazily sliced onions. I have tried. The diff is noticeable.
build_masala
Heat the pan on high and add the oil or ghee. If you are using the whole spices, they go in first, alone, for about fifteen seconds - this is the aroma bootloader, and you will smell exactly when it has finished initializing. Then add the onion-garlic-ginger paste, drop the heat to medium, and let it sweat for a good three minutes. The goal is soft and fragrant, not brown. Watch it. This is the step where the dish burns if you go check on something in another terminal.
Add all the ground spices and stir them through the paste for about half a minute so they toast and bloom in the fat. Then add the tomatoes, fry everything sharply for a moment, and simmer until the tomatoes have collapsed into the sauce. Add a splash of water and let it reduce back to a thick, glossy masala.
add_chickpeas
Drain the chickpeas and stir them in. If the sauce is too thick, loosen it with a little water; if it is too thin, let it simmer uncovered a bit longer. Taste, adjust salt and pepper, then put the lid on, drop the heat to low, and give it five minutes so the chickpeas take on the sauce. This idle loop is not optional. It is the difference between chickpeas in sauce and chana masala.
serve
Plate over basmati rice or with fresh roti, chapati, or naan. Top with fresh cilantro, sesame seeds, and crispy garlic. A yogurt-mint dip on the side handles any chili overprovisioning gracefully.
Status: 200 OK.
Post-Incident Notes
A few operational learnings from years of running this in production:
- The whole spices are marked optional in the script but I no longer build without them. The fifteen seconds of cumin seeds and cardamom in hot ghee changes the whole flavor baseline. Consider them optional the way monitoring is optional.
- It reheats better than it serves fresh. Like most curries, the flavor keeps integrating overnight. Cook double, and tomorrow’s lunch is a zero-effort deployment from the fridge.
- The chili powder is the only genuinely environment-specific value. Everything else in the spice array has been stable for years; the chili gets overridden per audience.
- HelloFresh deserves the credit for the initial commit. We forked the recipe, rewrote most of it, and stopped paying the subscription fee, which I suppose makes this the classic open-core story told from the other side. But the upstream project shipped a genuinely good v0.1, and this post is the attribution notice.
Do not deploy to production without seconds.
Comments
You can use your Mastodon or other ActivityPub account to comment on this article by replying to the associated post.
Search for the copied link on your Mastodon instance to reply.
Loading comments...