May 19, 2026

Shell reserved words are trickier than they look

Words like do, then, and in pass command -v in your shell. But they’re never valid as the first token of a standalone command. Getting this wrong breaks natural language detection.

The bug I didn’t see coming

Early in Lacy’s development, the detection logic was straightforward: check if the first word of the input is a valid shell command using command -v. If yes, route to shell. If no, check if it looks like natural language.

Then someone typed: do we already have a way to uninstall?

Lacy routed it to the shell. The shell threw an error. The user saw a syntax error instead of getting help from the AI agent.

The problem: do passes command -v. In fact, it exits 0 because do is a recognized shell keyword. But do is never valid as the first word of a standalone command. It only makes sense inside a for or while loop.

What command -v actually checks

command -v answers the question “does the shell recognize this word?” That includes:

  • External binaries on $PATH
  • Shell builtins (cd, echo, export)
  • Aliases
  • Functions
  • Shell reserved words (do, then, in, fi)

For the first four categories, a positive result from command -v means “this word can start a valid command.” For reserved words, it just means “the shell parser knows this token.” There’s no flag to distinguish between them.

This is documented behavior. POSIX says command -v identifies how the shell would interpret a word. Reserved words are identified, but they can’t stand alone.

The full list

Not every shell keyword is problematic. if, for, while, until, case, and time are excluded from Lacy’s reserved word filter because they can legitimately start commands or compound statements. A user typing if [ -f foo ]; then echo bar; fi is writing real shell code.

The words that are never valid as standalone invocations:

do  done  then  else  elif  fi  esac  in  select  function  coproc  { }  !  [[

Each of these is a continuation or closing token for a compound construct. You never type then as the first word of a command. You type it after if ... ;. But when someone types then what should I do next?, they’re asking a question.

Why this matters for natural language

English sentences frequently start with these words. Consider:

  • do we have a way to uninstall?
  • in the codebase where is the auth module?
  • then what should I do next?
  • done with the refactor, what else?

Without a reserved word filter, every single one of these gets routed to the shell. The shell throws a syntax error. The user has to rephrase their question.

This is the kind of friction that makes a tool feel broken. The user typed a perfectly natural question. They got a bash: syntax error near unexpected token instead of an answer.

The fix is three lines

Lacy’s detection function checks the reserved word list before calling command -v. Here’s the relevant section from lib/core/detection.sh:

# Layer 1a: Shell reserved words pass command -v
# but are never valid standalone commands.
if _lacy_in_list "$first_word" \
    "${LACY_SHELL_RESERVED_WORDS[@]}"; then
  echo "agent"
  return
fi

The reserved word list itself is defined in lib/core/constants.sh as a simple Bash array. Both the list and the detection algorithm are shared between Lacy’s shell plugin and lash (the AI CLI that Lacy recommends). Keeping them in sync means both tools agree on what counts as natural language.

Trailing punctuation

One more subtlety. Users often type do? or why? with trailing punctuation. The shell doesn’t strip punctuation before command -v, so do? would fail the reserved word check (it’s not in the list) and also fail command -v (not a recognized word).

Lacy strips trailing punctuation (?, !, ., ,, ;, :) from the first word before checking it against word lists. The original token (with punctuation) is still used for command -v so real commands are unaffected.

This means do? we have a setup guide routes to agent just like do we have a setup guide. Small detail, but users type punctuation. Ignoring that breaks real interactions.

The excluded keywords

The decision to exclude if, for, while, until, case, and time is intentional. These keywords can start compound commands. Someone typing for f in *.txt; do echo $f; done is writing a loop, not asking a question.

When a user types while you are at it fix the tests, the shell tries to parse it as a while loop. It fails with a syntax error. Lacy’s Layer 2 detection catches that failure, detects the natural language markers in the error context, and reroutes to the agent. I wrote about that mechanism in the post-execution reroute post.

The split is clean: reserved words that can never start a command go through the pre-execution filter. Keywords that sometimes start commands go through the post-execution fallback. Both layers together cover the full set of shell keywords.

What I learned

Shell syntax has sharp edges that show up only when you start treating user input as potentially ambiguous. command -v is the standard way to check if a word is a valid command, and it’s been the standard for decades. But it answers a different question than the one Lacy needs to ask.

Lacy doesn’t need to know “does the shell recognize this word?” It needs to know “could this word plausibly start a user-intended command?” That’s a subtly different question, and the answer requires a 15-element array that took weeks of real-world testing to get right.