MemberPress AI Academy memberpress.ai

Ship It · From Script to Feature

Wrap a call in a small CLI

Video15 min

A script that runs when you execute a file is a draft. A tool reads its input from the caller, reads secrets from the environment, prints only what is needed, and exits with a code the caller can act on.

The full CLI

# scripts/ask.py
import anthropic
import argparse
import os
import sys

def main():
    parser = argparse.ArgumentParser(description="Ask Claude a question.")
    parser.add_argument("--prompt", required=True, help="The user message.")
    parser.add_argument("--system", default="", help="Optional system prompt.")
    parser.add_argument("--max-tokens", type=int, default=512)
    parser.add_argument("--stream", action="store_true", help="Stream output.")
    args = parser.parse_args()

    key = os.getenv("ANTHROPIC_API_KEY")
    if not key:
        print("Error: ANTHROPIC_API_KEY is not set.", file=sys.stderr)
        sys.exit(1)

    client = anthropic.Anthropic()
    messages = [{"role": "user", "content": args.prompt}]

    if args.stream:
        with client.messages.stream(
            model="claude-opus-4-5",
            max_tokens=args.max_tokens,
            system=args.system,
            messages=messages,
        ) as stream:
            for text in stream.text_stream:
                print(text, end="", flush=True)
        print()
    else:
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=args.max_tokens,
            system=args.system,
            messages=messages,
        )
        print(response.content[0].text)

if __name__ == "__main__":
    main()

What each choice does

required=True on --prompt means the parser exits with a readable usage message if the argument is missing — before any network call is made.

The key check runs before the API client is initialized. If the variable is missing, the error is clear and immediate: Error: ANTHROPIC_API_KEY is not set. The sys.exit(1) instead of sys.exit(0) lets a shell script or CI job detect the failure.

print(text, end="", flush=True) in the streaming branch suppresses the newline between chunks so tokens accumulate on one line in the terminal, and flush=True pushes each chunk to the terminal immediately rather than buffering.

Test the exit codes

# Normal operation
python scripts/ask.py --prompt "What is entropy?" --system "Reply in one sentence."
echo $?  # → 0

# Missing key
ANTHROPIC_API_KEY="" python scripts/ask.py --prompt "Hello"
echo $?  # → 1

# Streaming
python scripts/ask.py --prompt "Count to five." --stream

Exit codes matter when this tool is called from a Makefile, a CI job, or another script. A silent 0 on failure silently corrupts the pipeline.

Composing with pipes

Because the tool writes to stdout and exits cleanly, it composes with standard Unix tools:

# Pipe a file into Claude
cat README.md | python scripts/ask.py --prompt "$(cat -)" --system "Summarize this in two sentences."

# Feed the output into another command
python scripts/ask.py --prompt "List five HTTP status codes." | grep "^[0-9]"

# Use it in a Makefile target
# Makefile
.PHONY: changelog
changelog:
    git log --oneline -20 | python scripts/ask.py \
      --prompt "$(shell git log --oneline -20)" \
      --system "Turn these git commits into a changelog entry. Plain text, no markdown." \
      > CHANGELOG_ENTRY.txt

The pipe pattern works because the tool does not print anything to stdout except the Claude response. No "Calling Claude..." prefix, no status lines, no token counts on stdout. Status and debug output go to stderr.

Make it executable

Add a shebang line at the top of the file:

#!/usr/bin/env python3

Then run chmod +x scripts/ask.py. Now ./scripts/ask.py --prompt "Hello" works without the python prefix. Add the scripts/ directory to your PATH and you have a system-wide tool.

If you get stuck

  • argparse exits with code 2 on bad input: that is correct behavior. Code 2 conventionally means usage error in Unix tools.
  • Streaming output is buffered in a terminal: confirm flush=True is set. You can also run with python -u to disable Python's output buffering globally.
  • The shebang line is ignored: the file may not be executable. Run chmod +x scripts/ask.py and confirm with ls -l scripts/ask.py.
  • anthropic.Anthropic() ignores the key: the SDK reads ANTHROPIC_API_KEY from the environment automatically. You do not need to pass it to the constructor — just confirm the variable is set before instantiating.
  • Pipes truncate output: confirm nothing before the streaming loop is writing to stdout. A stray print() before the loop can break the pipe contract.

Cross-reference

l3 (Streaming vs. blocking) explains the streaming event model the --stream flag builds on. l8 (Add it to a real app) takes the same call and embeds it in an HTTP route with structured output and error handling.

Deliverable

python scripts/ask.py --prompt "What is entropy?" --system "Reply in one sentence." returns a single sentence and exits 0. Running with ANTHROPIC_API_KEY="" exits 1 with a message on stderr and makes no network call. Piping the output into wc -w prints a word count without error.

Try these prompts

  • Write the argparse setup for a CLI that accepts --prompt, --system, --max-tokens, and --stream flags, with --prompt required.
  • Write the shell one-liner that pipes a file into the CLI and feeds Claude's output to wc -w.
  • The CLI should exit with code 1 if ANTHROPIC_API_KEY is not set, before making any network call. Write that check.