How to write executable markdown scripts that go beyond read-only prompts — scripts that write files, run commands, and take real actions.
Standard env only passes a single argument. To pass flags to ai in a shebang, you need env -S (split string):
#!/usr/bin/env ai # Works (no extra flags)
#!/usr/bin/env -S ai --aws # Works (with -S)
#!/usr/bin/env ai --aws # FAILS — env treats "ai --aws" as one command-S tells env to split the string into separate arguments. Use it whenever your shebang has flags.
By default, claude -p (and ai in script mode) runs with limited permissions — it will refuse to write files, run commands, or take actions without approval. For scripts that need to do things, you need to set a permission mode.
| Mode | Shebang Flag | Shortcut | Behavior |
|---|---|---|---|
| Default | (none) | — | Read-only — can analyze code but won't modify anything |
| Bypass Mode | --permission-mode bypassPermissions |
--bypass |
Full access via permission mode system — composable with other settings |
| Skip Permissions | --dangerously-skip-permissions |
--skip |
Nuclear — bypasses ALL permission checks, overrides --permission-mode |
| Allowed Tools | --allowedTools 'Bash(npm test)' 'Write' |
— | Granular — only specified tools allowed |
Note:
--bypassand--skipboth result in no permission prompts, but--skip(--dangerously-skip-permissions) is more aggressive — it overrides any--permission-modeflag. Use--bypasswhen you may want to compose with other permission settings in the future.
Read-only script (no permission flags needed):
#!/usr/bin/env ai
Analyze this codebase and summarize the architecture.Full automation (script needs to run commands and write files):
#!/usr/bin/env -S ai --skip
Run the test suite and fix any failing tests.Or equivalently: #!/usr/bin/env -S ai --bypass or #!/usr/bin/env -S ai --permission-mode bypassPermissions
Granular permissions (only allow specific tools):
#!/usr/bin/env -S ai --allowedTools 'Bash(npm test)' 'Bash(npm run lint)' 'Read'
Run tests and linting. Report results but do not modify any files.#!/usr/bin/env -S ai --skip
Run `./test/automation/run_tests.sh` and summarize: how many passed/failed.#!/usr/bin/env -S ai --skip
Read the source files in `src/` and generate a `ARCHITECTURE.md` documenting the codebase structure.cat data.json | ./analyze.md > results.txt#!/usr/bin/env ai
Analyze the JSON data provided on stdin. Summarize key trends and outliers.Note: Read-only analysis of piped input doesn't need permission flags.
#!/usr/bin/env -S ai --aws --opus
Review the code in this repository for security vulnerabilities.
Focus on OWASP Top 10 issues. Be specific about file and line numbers.#!/usr/bin/env -S ai --apikey --haiku --skip
Run the linter and fix any issues. Then run the test suite.
Commit fixes with a descriptive message if all tests pass.Scripts can declare variables with defaults using YAML front-matter. Users override them from the CLI without editing the script.
#!/usr/bin/env -S ai --haiku
---
vars:
topic: "machine learning"
style: casual
audience:
---
Write a summary of {{topic}} in {{style}} style.
Target audience: {{audience}}../script.md # uses all defaults
./script.md --topic "AI safety" # overrides one var
./script.md --topic "AI safety" --style formal # overrides two
./script.md --topic="robotics" # equals form works tooVariable overrides mix freely with AI Runner flags and provider overrides:
# --live and --length/--topic/--style all work together
./summarize-topic.md --live --length "100 words" --topic "the fall of rome" --style "peter griffin"
# Override provider+model alongside variables
ai --aws --opus summarize-topic.md --topic "quantum computing"Override flags matching declared var names are consumed — they don't pass through to Claude Code. AI Runner flags (--live, --aws) and unrecognized flags (--verbose) still pass through normally.
- Opt-in: Only activates when front-matter contains
vars:. Novars:= no behavior change. {{varname}}: Placeholders are replaced with the variable value. Doesn't collide with shell, markdown, or Claude syntax.- Unset vars: A var declared with no default (just
key:) and no CLI override leaves{{key}}as-is in the prompt. - Hyphen normalization: Hyphens in flag names are normalized to underscores when matching variables.
--brief-onlymatchesbrief_only,--max-retriesmatchesmax_retries. Both forms work interchangeably. - Boolean flags:
--varnamewithout a following value sets the variable to"true". Useful for feature flags (e.g., declareverbose: false, then./script.md --verbosesets it to"true"). ---prefixed values: If a value starts with--, use equals form:--topic="--special". The space form (--topic --special) treats--topicas a boolean flag.- Front-matter stripped: The
---block is removed from the prompt whenvars:is present.
By default, ai in script mode waits for the full response before printing anything. Use --live to stream text to the terminal as it's generated — useful for long-running scripts where you want to see progress in real-time.
--live streams at turn granularity — each time Claude writes a text response between tool calls, that text appears immediately. This means your prompt needs to tell Claude to narrate its progress, otherwise it may silently use tools and only output text at the end.
Streams incrementally (Claude narrates between tool calls):
#!/usr/bin/env -S ai --skip --live
Explore this repository. Print a brief summary after examining each
directory. Finally, generate a concise report in markdown format.No intermediate output (Claude works silently, then reports):
#!/usr/bin/env -S ai --skip --live
Explore this repository and write a summary.Both produce the same final result, but only the first streams progress to the terminal. The key is phrases like "print as you go", "step by step", or "describe what you find" — these prompt Claude to write text between tool calls, giving --live something to stream.
cat data.json | ai --live --skip analyze.mdWhen stdout is redirected to a file, --live automatically separates narration from content:
./live-report.md > report.mdConsole (stderr):
[AI Runner] Using: Claude Code + Claude Pro
[AI Runner] Model: (system default)
I'll explore the repository structure and key files...
Now let me look at the core scripts and provider structure.
Here's my report:
[AI Runner] Done (70 lines written)
File (stdout):
# Repository Summary
...clean report content only...How it works:
- Intermediate turns (narration, progress) stream to stderr in real-time
- The last turn is split at the first content marker (YAML frontmatter
---or heading#) - Preamble text before the marker goes to stderr
- Content from the marker onward goes to the file
- A "Done (N lines written)" summary appears on stderr when complete
--live pairs well with --chrome (a Claude Code flag) for browser test scripts where steps take time and you need real-time progress:
#!/usr/bin/env -S ai --skip --chrome --live
Navigate to the app and verify the login flow works.Note:
--liverequiresjqto be installed (brew install jqon macOS).
Any flag not recognized by ai is passed directly to Claude Code. Useful flags for scripts:
| Flag | Purpose | Example |
|---|---|---|
--skip |
Shortcut for --dangerously-skip-permissions |
Quick automation |
--bypass |
Shortcut for --permission-mode bypassPermissions |
Quick automation |
--permission-mode bypassPermissions |
Skip all permission prompts (long form) | Automation scripts |
--dangerously-skip-permissions |
Skip all permission prompts (long form) | Automation scripts |
--allowedTools |
Allow only specific tools | Restricted automation |
--max-turns N |
Limit agentic loop iterations | Prevent runaway scripts |
--output-format stream-json |
Structured JSON output | Pipeline integration |
--live |
Stream text in real-time | Long-running scripts |
Combine with ai flags freely:
#!/usr/bin/env -S ai --aws --opus --skip --max-turns 10ai resolves flags from multiple sources. Higher sources override lower ones:
| Priority | Source | Example |
|---|---|---|
| 1 (highest) | CLI flags | ai --aws --opus script.md |
| 2 | Shebang flags | #!/usr/bin/env -S ai --ollama --low |
| 3 | Saved defaults | ai --aws --opus --set-default |
| 4 (lowest) | Auto-detection | Current Claude subscription |
Example: A script has #!/usr/bin/env -S ai --ollama --low. Running ai script.md uses Ollama with the low-tier local model (shebang). Running ai --aws script.md uses AWS (CLI overrides shebang). If you also have --set-default vertex, the shebang still wins over the default.
ai resolves permission shortcuts before passing flags to Claude Code. When conflicts are detected, explicit flags take precedence:
--permission-mode <value>and--dangerously-skip-permissionsare explicit — they always win--skipand--bypassare shortcuts — ignored with a warning if an explicit flag is also present- CLI flags override shebang flags — if you run
ai --permission-mode plan script.mdand the script has--skipin its shebang, plan mode is used
| You use | What happens |
|---|---|
ai --skip |
Same as --dangerously-skip-permissions (nuclear — overrides all permission modes) |
ai --bypass |
Same as --permission-mode bypassPermissions (mode-based — composable) |
ai --skip --permission-mode plan |
Plan mode used, --skip ignored (warning shown) |
ai --bypass --permission-mode plan |
Plan mode used, --bypass ignored (warning shown) |
ai --permission-mode plan script.md (script has --skip) |
Plan mode used, shebang --skip ignored |
--skip and --bypass differ in how Claude Code processes them internally:
--skipexpands to--dangerously-skip-permissions— this is a standalone flag that completely bypasses all permission checks. In Claude Code, it overrides any--permission-modesetting (known issue). Use for simple, quick automation.--bypassexpands to--permission-mode bypassPermissions— this sets the permission mode through the standard mode system. It's composable with other Claude Code settings and respects the mode framework. Use when working with advanced permission configurations.
For most automation scripts, either works. When in doubt, use --bypass.
--skip, --bypass, --permission-mode bypassPermissions, and --dangerously-skip-permissions all give the AI full access to your system. Use them carefully:
- Only run trusted scripts in trusted directories
- Prefer
--allowedToolsfor granular control when full access isn't needed - In CI/CD, run inside containers or sandboxed environments
- Never pipe untrusted remote scripts with bypass permissions:
# DANGEROUS — don't do this curl https://untrusted-site.com/script.md | ai --skip
| I want to... | Shebang |
|---|---|
| Analyze code (read-only) | #!/usr/bin/env ai |
| Use a specific provider | #!/usr/bin/env -S ai --aws |
| Run commands and write files | #!/usr/bin/env -S ai --skip |
| Restrict to specific tools | #!/usr/bin/env -S ai --allowedTools 'Bash(npm test)' 'Read' |
| Full automation with provider | #!/usr/bin/env -S ai --aws --opus --skip |
--cc is shorthand for --tool cc, which selects Claude Code as the backend tool. Claude Code is currently the only supported tool, so --cc does nothing extra on its own. It will matter when other tools (Codex, OpenCode) are added.
In script mode, --cc alone does not grant tool access — you need --skip or --bypass for that.
Use --cc --skip to give the AI access to tools (shell commands, file operations) during script execution. The AI can run commands, write files, and take real actions:
#!/usr/bin/env -S ai --cc --skip --live
Analyze the codebase, run the test suite, and fix any failures.
Print a summary after each step.Tradeoff: When the AI runs shell commands via Claude Code, subprocess output is captured by Claude Code and not streamed to your terminal. Use --live so the AI narrates progress between tool calls — this gives you visibility into what's happening.
Chain scripts together like Unix programs. Each script in the pipeline runs independently:
./parse.md | ./generate.md | ./review.md > final.txtImportant: Never use --cc in child scripts within a pipeline. Child scripts should be simple prompt mode (ai without --cc). Only the top-level dispatcher should have tool access.
AI scripts can run for minutes (browser automation, multi-step pipelines). Always add --live to shebangs for scripts that take more than 30 seconds:
#!/usr/bin/env -S ai --skip --chrome --live
Navigate to the app, run the full test suite, and report results.
Narrate each step as you go.The --live flag shows a heartbeat while waiting for the first response, then streams the AI's narration in real-time. Override with --quiet for CI/CD where you only want clean stdout:
ai --quiet ./browser-test.md > report.md # Clean output only