diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md
index a91fef6..df651ce 100644
--- a/IMPLEMENTATION_PLAN.md
+++ b/IMPLEMENTATION_PLAN.md
@@ -54,6 +54,11 @@ priorities.
Stop the Bash and PowerShell native greedy passes before a token that
matches the shared path-shape rules. Preserve that token as a resolved
argument without a command dictionary or a public API change.
+- [x] **Consumer guide.** Added `docs/CONSUMER_GUIDE.md` with the
+ shell-neutral security-consumer algorithm, Bash and PowerShell guidance,
+ worked public use cases, and immutable permalinks to Netclaw's production
+ integration. Linked it from the README and aligned stale PowerShell
+ prerelease/status wording in the public project docs.
- [x] **Issue #52 — hyphenated PowerShell parameters/native options.**
Preserve internal hyphens, apply bash-compatible native
`--flag=value` splitting and path classification, keep colon binding
@@ -85,8 +90,6 @@ priorities.
- Seed corpus entries from sanitized real-world dogfood logs (SPEC §14
workflow) — both shells.
- Expand verb / cmdlet / alias tables as the corpus surfaces real commands.
-- Document the "consumer's algorithm" — given a `ParsedCommand`, how a
- security gate walks it (a `docs/CONSUMER_GUIDE.md` or `SPEC.md` appendix).
- Performance sanity check (~1 ms typical) with a tiny BenchmarkDotNet
harness — only if anything in the daemon hot path complains.
diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md
index 0c05623..f435404 100644
--- a/PROJECT_CONTEXT.md
+++ b/PROJECT_CONTEXT.md
@@ -19,24 +19,29 @@ The output is a `ParsedCommand` containing:
when known, and explicit `DynamicSkip` marking for unresolved env vars
/ unexpanded globs
- redirect operators (`>`, `>>`, `<`, `2>`, `2>>`)
-- `cd
&& cmd` propagation — the cd target is attributed to subsequent
- clauses in the same compound
-- recursion into `bash -c ""` so wrapped commands surface as clauses
-- subshell `( ... )` isolation for `cd` attribution
+- Bash `cd && cmd` and PowerShell `Set-Location ; cmd`
+ propagation — the target is attributed to subsequent clauses
+- recursion into `bash -c`, `pwsh -Command`, and `pwsh -EncodedCommand` so
+ wrapped commands surface as clauses
+- PowerShell alias canonicalization and explicit dynamic-command identity
+- Bash subshell isolation and PowerShell grouping semantics for cwd attribution
- safe-fail flag `IsUnparseable` for unsupported constructs (control flow,
- function definitions, process substitution, deep `bash -c` nesting,
- unbalanced quotes/parens)
+ function definitions, process substitution, deep command-string nesting,
+ unbalanced quotes/parens/opaque regions)
-The complete contract lives in [`SPEC.md`](./SPEC.md).
+The complete contract lives in [`SPEC.md`](./SPEC.md) and
+[`SPEC.POWERSHELL.md`](./SPEC.POWERSHELL.md).
## Who It's For
**Primary consumer: [Netclaw](https://github.com/netclaw-dev/netclaw)** — an
-open-source autonomous operations agent. Netclaw's approval gate currently
-hand-rolls equivalent functionality in
-`src/Netclaw.Security/ShellApprovalSemantics.cs` and `ShellTokenizer.cs`.
-ShellSyntaxTree v0.1 is intended to **replace that hand-rolled approximation**
-with a richer, structured AST that Netclaw's gate can walk directly.
+open-source autonomous operations agent. Netclaw's POSIX approval gate consumes
+ShellSyntaxTree's Bash parser to decompose approval units, identify candidate
+verbs and directories, propagate cwd context, inspect redirects, and fail
+closed when parsing is uncertain. Its PowerShell integration is the remaining
+v0.2.0 downstream acceptance item. See
+[`docs/CONSUMER_GUIDE.md`](./docs/CONSUMER_GUIDE.md) for the public consumer
+algorithm and immutable Netclaw examples.
**Acceptance is tied to Netclaw integration** (see SPEC §17 #7-#8): the
package must be consumable via `` and exercise at least
@@ -56,15 +61,16 @@ zero-native-deps .NET parser sized to what security gates actually need.
## Scope Discipline
-### v0.1 (current)
+### v0.2 (current prerelease line)
-- Bash only. PowerShell and Windows `cmd` are deferred — but the
- `IShellParser` seam is in place so consumers don't have to refactor.
+- Bash and PowerShell 7 pipeline parsing ship behind the shared
+ `IShellParser` seam. Windows `cmd` remains deferred.
- Public API surface in SPEC §2 is **locked**. Internal changes are free.
-- Acceptance is the corpus contract (SPEC §13): every JSON entry parses to
- its expected AST.
+- Acceptance is the multi-shell corpus contract: every Bash and PowerShell
+ JSON entry parses to its expected AST, and the PowerShell corpus also passes
+ the live `pwsh` oracle matrix.
-### Explicit non-goals (v0.1)
+### Explicit non-goals
- Command execution.
- Variable expansion of any kind (we **mark** dynamic tokens, never resolve
@@ -73,25 +79,30 @@ zero-native-deps .NET parser sized to what security gates actually need.
- Process substitution `<(cmd)`, `>(cmd)`.
- Function definitions, `for`/`while`/`case` control flow, arithmetic
expansion `$((...))`.
+- PowerShell script-level control flow, definitions, expression evaluation,
+ and `.ps1` file-content parsing.
- Performance optimization beyond "fast enough to invoke per shell call
without noticeable latency" (~1 ms typical).
-- Source-mapping (line/column for AST nodes — useful for IDEs, irrelevant
- for security gates).
+- Full IDE-style source mapping. Security-motivated token provenance is under
+ active design in issue #62.
### Versioning
-- `v0.1.0-alpha` — first publishable cut, bash-only.
-- `v0.1.x` — additive (more verb table entries, more corpus, bug fixes).
-- `v0.2.0` — first PowerShell parser implementation.
-- `v1.0.0` — at least one external consumer beyond Netclaw ships against it
+- `0.1.0-alpha` — first publishable cut, Bash-only.
+- `0.1.x` — additive (more verb table entries, more corpus, bug fixes).
+- `0.2.0` — first PowerShell parser implementation; alpha and beta.1 shipped,
+ stable promotion pending downstream validation.
+- `1.0.0` — at least one external consumer beyond Netclaw ships against it
without finding API gaps.
## Architectural Constraints
- **Public API in `SPEC.md` §2 is the contract.** Everything else is
- `internal`. Renaming/removing public fields requires a major version bump.
-- **`IShellParser` is the multi-shell seam.** PowerShell and `cmd` parsers
- must be addable without touching consumer code.
+ `internal`. During `0.x`, renaming or removing public fields requires a
+ deliberate minor version bump and migration notes; after `1.0`, it requires
+ a major version bump.
+- **`IShellParser` is the multi-shell seam.** Additional parsers such as
+ Windows `cmd` must be addable without reshaping consumer code.
- **No native dependencies.** AOT-trim friendly; ship a single managed
package.
- **Multi-targeting**: `netstandard2.0` for broad consumer reach, `net8.0`
@@ -100,18 +111,20 @@ zero-native-deps .NET parser sized to what security gates actually need.
`IsUnparseable`. Consumers can always relax — they cannot retroactively
un-execute a command we falsely classified as safe.
-## Acceptance for v0.1.0-alpha
+## Shipped acceptance for 0.1.0-alpha
Per SPEC §17, all of the following must be true:
1. Public API matches SPEC §2 exactly. `dotnet pack` produces a
`ShellSyntaxTree.0.1.0-alpha.nupkg`.
-2. Every corpus entry in `tests/Corpus/bash/*.json` parses to its expected
+2. Every corpus entry in
+ `tests/ShellSyntaxTree.Tests/Corpus/bash/*.json` parses to its expected
AST. `dotnet test` runs them all and passes.
3. Corpus has ≥ 105 entries spanning the SPEC §13 categories.
-4. PII audit scan over `tests/Corpus/bash/*.json` finds zero hits.
+4. PII audit scan over `tests/ShellSyntaxTree.Tests/Corpus/bash/*.json` finds
+ zero hits.
5. PR validation runs on GitHub Actions and passes.
-6. Tagging `v0.1.0-alpha` triggers `publish_nuget.yml` and the package
+6. Tagging `0.1.0-alpha` triggers `publish_nuget.yml` and the package
appears on nuget.org.
7. Netclaw consumes the package via `` and `IShellParser`
resolves at runtime in Netclaw's DI container.
@@ -124,27 +137,28 @@ Per SPEC §17, all of the following must be true:
logs (`~/.netclaw/logs/daemon-*.log`) that contain usernames, repo paths,
channel/thread IDs. Sanitization is mandatory and gated by a CI scan
(SPEC §14). Shipping unsanitized PII is a release-blocker.
-- **Verb table drift**. `BashArity`, `FileVerbs`, `CwdVerbs`, and
- `FlagsWithValue` are static data. Adding entries is cheap; *missing*
- entries means the parser silently mis-classifies. The corpus is the
+- **Verb/binding table drift**. Bash verb tables and PowerShell alias,
+ binding, and per-verb tables are static data. Adding entries is cheap;
+ missing entries can misclassify. The corpora and `pwsh` oracle are the
early-warning system.
-- **`bash -c` recursion depth**. Capped at 5 (SPEC §10). Going deeper
- marks the deepest clause `IsUnparseable` so we don't blow the stack on
- hostile inputs.
+- **Command-string recursion depth**. `bash -c`, `pwsh -Command`, and
+ `pwsh -EncodedCommand` recursion is capped at 5. Going deeper marks the
+ result `IsUnparseable` rather than risking hostile recursion.
- **Resolver assumptions**. `WorkingDirectory` defaults to the daemon's
cwd. If consumers pass the wrong cwd, relative-path attribution will
- silently disagree with what bash would do at runtime. Document loudly.
-- **API surface lock**. v0.1 commits to the AST shape. Mistakes here cost
- a major-version bump to fix.
+ silently disagree with what the selected shell would do at runtime.
+- **API surface lock**. The `0.x` line commits to the documented AST shape;
+ breaking changes require a deliberate minor bump and migration notes.
## Where Things Live
| Concern | Location |
|---|---|
-| Library source | `src/ShellSyntaxTree/` *(to be created)* |
-| Tests + corpus | `tests/ShellSyntaxTree.Tests/` *(to be created)* |
-| Corpus entries | `tests/ShellSyntaxTree.Tests/Corpus/bash/*.json` |
-| The contract | `SPEC.md` |
+| Library source | `src/ShellSyntaxTree/` |
+| Tests + corpus | `tests/ShellSyntaxTree.Tests/` |
+| Corpus entries | `tests/ShellSyntaxTree.Tests/Corpus/{bash,powershell}/*.json` |
+| The contracts | `SPEC.md`, `SPEC.POWERSHELL.md` |
+| Consumer guide | `docs/CONSUMER_GUIDE.md` |
| Active work plan | `IMPLEMENTATION_PLAN.md` |
| Tooling inventory | `TOOLING.md` |
| Agent constitution | `AGENTS.md` (and `CLAUDE.md`) |
diff --git a/README.md b/README.md
index 373d1e7..5f540f2 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,12 @@ Hand-rolled, AOT-trim friendly, zero native dependencies. Multi-targets
`netstandard2.0` and `net8.0`.
```bash
-dotnet add package ShellSyntaxTree --version 0.2.0-alpha
+dotnet add package ShellSyntaxTree --version 0.2.0-beta.1
```
+The `0.2.0-beta.1` prerelease is required for PowerShell support. The latest
+stable `0.1.x` package supports Bash only.
+
## What you get
For an input like `cd /repo && rm /etc/passwd`, ShellSyntaxTree produces:
@@ -95,6 +98,14 @@ AndIf rm
path: /etc/passwd
```
+## Consumer guide
+
+The [consumer guide](./docs/CONSUMER_GUIDE.md) develops the quick start into
+a production-oriented algorithm: parser selection, safe-fail handling, command
+identity, paths and cwd attribution, redirects, compounds and pipelines, and
+PowerShell-specific alias and dynamic-command behavior. It also links to
+immutable examples from Netclaw's live approval-gate integration.
+
## Public API surface
```csharp
@@ -146,7 +157,7 @@ policy ("deny writes in `/etc`, `/usr`, `/bin`, `/sbin`, `/lib`",
"warn on `curl | bash`", "warn on dynamic args in path slots") and
exits 0 / 1 / 2 by severity. See
[`samples/ShellSyntaxTree.Cli.Sample/Commands/AuditPolicy.cs`](./samples/ShellSyntaxTree.Cli.Sample/Commands/AuditPolicy.cs)
-for the policy code — ~50 lines.
+for the illustrative policy code.
### `ShellSyntaxTree.Web.Sample` — Blazor WebAssembly Mermaid visualizer
@@ -163,11 +174,11 @@ dotnet run --project samples/ShellSyntaxTree.Web.Sample

-A shell selector switches between the bash and PowerShell parsers; each
-ships preset scripts demonstrating compound commands, subshell isolation,
-command-string recursion, alias resolution, dynamic-cwd attribution, and
-unparseable inputs. Each preset shows what the library produces in a
-single click.
+A shell selector switches between the bash and PowerShell parsers. The presets
+demonstrate compound commands, Bash subshell isolation, PowerShell grouping and
+alias resolution, command-string recursion, dynamic-cwd attribution, and
+unparseable inputs. Each preset shows what the library produces in a single
+click.
## Building from source
@@ -210,6 +221,7 @@ workflow asserts this and fails fast on misformatted tags.
| `tests/ShellSyntaxTree.Tests/Corpus//*.json` | Corpus entries — the acceptance contract (bash + powershell) |
| `samples/ShellSyntaxTree.Cli.Sample/` | Console explainer + audit policy |
| `samples/ShellSyntaxTree.Web.Sample/` | Blazor WASM Mermaid visualizer |
+| `docs/CONSUMER_GUIDE.md` | Production-oriented consumer algorithm + Netclaw case study |
| `tools/PwshCorpusTool/` | PowerShell corpus authoring aid |
| `SPEC.md`, `SPEC.POWERSHELL.md` | The behavioral contract |
| `openspec/` | Change-proposal history (rationale for design decisions) |
diff --git a/SPEC.POWERSHELL.md b/SPEC.POWERSHELL.md
index f2f4737..d589e0c 100644
--- a/SPEC.POWERSHELL.md
+++ b/SPEC.POWERSHELL.md
@@ -1,8 +1,9 @@
# ShellSyntaxTree — PowerShell Specification (v0.2.0)
-**Status:** Draft for v0.2.0. Approved decisions; implementation pending.
-**Audience:** Whoever (human or agent) implements the ShellSyntaxTree
-PowerShell parser.
+**Status:** Shipped in the v0.2.0 prerelease line; stable promotion pending
+downstream Netclaw validation.
+**Audience:** Whoever (human or agent) implements, consumes, or maintains the
+ShellSyntaxTree PowerShell parser.
**Read `SPEC.md` (the bash and shared-contract specification) end-to-end
first — this document specifies only what differs for PowerShell.**
diff --git a/docs/CONSUMER_GUIDE.md b/docs/CONSUMER_GUIDE.md
new file mode 100644
index 0000000..83ea585
--- /dev/null
+++ b/docs/CONSUMER_GUIDE.md
@@ -0,0 +1,430 @@
+# Consuming ShellSyntaxTree
+
+ShellSyntaxTree turns a shell command into facts that another system can use
+without executing the command. It is designed for approval gates, CI/CD
+auditors, sandbox planners, audit-log processors, and other tools that need to
+reason about commands before or after execution.
+
+The library is deliberately not a policy engine. It reports clauses, candidate
+verb chains, arguments, paths, redirects, inherited working directories, and
+uncertainty. A consumer decides what those facts mean for its own domain.
+
+## The boundary between parsing and policy
+
+ShellSyntaxTree owns shell syntax:
+
+- splitting compounds and pipelines into clauses;
+- recognizing Bash commands, PowerShell cmdlets, aliases, and native commands;
+- resolving path-shaped arguments against the caller-supplied working directory;
+- propagating `cd` / `Set-Location` working-directory context;
+- surfacing redirects and command-string wrappers;
+- marking dynamic or unsupported input so a security consumer can fail safely.
+
+The consumer owns policy:
+
+- which verbs or cmdlets are allowed;
+- how broad an approval pattern should be;
+- which filesystem zones are trusted;
+- whether pipelines are displayed as one approval unit or several;
+- executable-specific meaning, such as the difference between a Git global
+ option and a `git commit` option;
+- the final `ALLOW`, `PROMPT`, or `DENY` decision.
+
+This separation is important. ShellSyntaxTree cannot safely embed the grammar
+of every executable. It should preserve the source facts a command-aware
+consumer needs, while remaining conservative when those facts are incomplete.
+
+```mermaid
+flowchart TD
+ A["Command text + selected shell + working directory"] --> B
+ B{"Caller selects the matching parser"}
+
+ subgraph SST["ShellSyntaxTree"]
+ C["BashParser"]
+ D["PwshParser"]
+ C --> E["Parse syntax, classify tokens, and resolve static context"]
+ D --> E
+ E --> F["ParsedCommand: ordered clauses, verbs, args, redirects, cwd, and uncertainty"]
+ end
+
+ subgraph APP["Consumer-owned policy"]
+ G{"IsUnparseable or policy-relevant input dynamic?"}
+ G -->|Yes| H["Safe-fail: prompt or deny"]
+ G -->|No| I["Walk every clause in source order"]
+ I --> J["Choose command identity and evaluate paths, cwd, and redirects"]
+ J --> K{"ALLOW / PROMPT / DENY"}
+ end
+
+ B -->|Bash| C
+ B -->|PowerShell| D
+ F --> G
+```
+
+The diagram is a responsibility flow, not an execution flow: parsing never
+runs the command, and every decision after `ParsedCommand` belongs to the
+consumer.
+
+## A production-shaped consumer loop
+
+The caller should know which shell will execute the command and select that
+parser explicitly. Supplying the real working directory is equally important:
+relative paths are resolved against it.
+
+```csharp
+using ShellSyntaxTree;
+
+static IShellParser CreateParser(string shell, string workingDirectory) =>
+ shell switch
+ {
+ "bash" => new BashParser(new BashParserOptions
+ {
+ WorkingDirectory = workingDirectory,
+ }),
+ "pwsh" => new PwshParser(new PwshParserOptions
+ {
+ WorkingDirectory = workingDirectory,
+ }),
+ _ => throw new ArgumentOutOfRangeException(nameof(shell)),
+ };
+```
+
+Do not guess the shell from the command text. `rm`, `cd`, quoting, redirects,
+and grouping can mean different things in Bash and PowerShell.
+
+Once parsed, a security-oriented consumer normally follows this sequence:
+
+1. Reject or prompt on an unparseable result.
+2. Walk every clause; do not authorize only the first stage of a compound or
+ pipeline.
+3. Determine a conservative command identity.
+4. Evaluate explicit path arguments, inherited cwd attribution, and redirect
+ targets.
+5. Elevate dynamic or unresolved content when it affects the policy decision.
+6. Apply product-specific rules and produce a decision.
+
+The decision types and `Evaluate*` helpers below are application-owned
+placeholders. ShellSyntaxTree supplies the parsed facts, not those policy APIs.
+
+```csharp
+var parser = CreateParser(shell, workingDirectory);
+var parsed = parser.Parse(command);
+
+if (parsed.IsUnparseable)
+{
+ // Partial clauses are diagnostic evidence, not authorization evidence.
+ return GateDecision.Prompt(parsed.UnparseableReason ?? "unsupported command shape");
+}
+
+foreach (var clause in parsed.Clauses)
+{
+ if (clause.Verb.IsDynamic)
+ {
+ return GateDecision.Prompt("command identity is dynamic");
+ }
+
+ var gateKey = GetGateKey(clause.Verb);
+ if (gateKey is null)
+ {
+ return GateDecision.Prompt("clause has no statically known command");
+ }
+
+ var decision = EvaluateClause(gateKey, clause);
+ if (decision.Outcome != GateOutcome.Allow)
+ {
+ return decision;
+ }
+}
+
+return GateDecision.Allow();
+```
+
+The example returns on the first non-allow result for brevity. A real UI may
+collect every clause decision so the operator can see the complete command.
+
+## Choosing a command identity
+
+For PowerShell aliases, prefer the canonical cmdlet identity while retaining
+the token the user typed for display:
+
+```csharp
+static string? GetGateKey(VerbChain verb)
+{
+ if (verb.IsDynamic || verb.Tokens.Count == 0)
+ {
+ return null;
+ }
+
+ return verb.CanonicalVerb ?? verb.Tokens[0];
+}
+```
+
+For example, parsing `gci C:\logs` preserves `gci` in `Tokens` and reports
+`Get-ChildItem` in `CanonicalVerb`. A policy can gate on `Get-ChildItem`; an
+audit UI can still show `gci`.
+
+`VerbChain` is a best-effort syntactic hint, not a complete executable grammar.
+The greedy native-command walk can include bare lowercase values because a
+generic parser cannot know whether `origin` is a Git remote or a subcommand.
+Unknown commands should therefore default to the full extracted chain, which
+produces narrower approvals and recoverable re-prompts. A consumer may shorten
+the chain only when it owns command-specific knowledge that justifies doing so.
+
+## Evaluating arguments and paths
+
+An `Arg` carries several independent facts:
+
+- `Raw` is the user-facing token;
+- `IsFlag` identifies option-shaped tokens;
+- `Kind` describes literal, environment-variable, glob, tilde, or dynamic
+ content;
+- `IsPath` says the parser classified the argument position as a path;
+- `Resolved` carries a normalized path when static resolution was possible;
+- `IsCwdAttribution` marks derived working-directory context rather than a
+ token written in that clause.
+
+These facts should not be collapsed into one boolean decision. A typical zone
+policy might handle them as follows:
+
+```csharp
+foreach (var arg in clause.Args)
+{
+ if (arg.IsCwdAttribution)
+ {
+ EvaluateInheritedDirectory(arg.Resolved, arg.Kind);
+ continue;
+ }
+
+ if (arg.Kind == ArgKind.DynamicSkip)
+ {
+ EvaluateUnknownArgument(arg.Raw);
+ continue;
+ }
+
+ if (!arg.IsPath)
+ {
+ continue;
+ }
+
+ if (arg.Kind == ArgKind.Glob)
+ {
+ EvaluateGlobCoveringDirectory(arg.Raw);
+ continue;
+ }
+
+ EvaluatePath(arg.Resolved ?? arg.Raw);
+}
+```
+
+The policy decides whether an unknown argument matters. `echo $message` may be
+acceptable to one product, while `Remove-Item $target` should normally prompt.
+Never treat `DynamicSkip.Raw` as a statically resolved path.
+
+### Working-directory attribution
+
+For `cd /repo && cat file.txt`, the `cat` clause receives a synthetic
+`IsCwdAttribution` argument for `/repo`, and `file.txt` resolves against that
+directory. PowerShell provides the same contract for `Set-Location` and its
+aliases.
+
+The attributed argument is derived context:
+
+- use it when evaluating where a clause operates;
+- do not render it as text the user wrote in that clause;
+- treat a dynamic cwd attribution as unknown context and prompt rather than
+ falling back to the process cwd.
+
+Bash subshells isolate cwd changes. PowerShell parenthesized pipelines do not:
+`(Set-Location C:\repo); Get-ChildItem` changes runspace location, so the later
+clause inherits that attribution.
+
+## Evaluating redirects
+
+Redirect targets are operands too. A command that appears path-free can still
+write outside an allowed zone:
+
+```text
+echo safe > /etc/profile.d/example.sh
+```
+
+Walk `Clause.Redirects` independently of `Args`:
+
+```csharp
+foreach (var redirect in clause.Redirects)
+{
+ if (redirect.IsDynamicSkip)
+ {
+ EvaluateUnknownRedirect(redirect.Target);
+ continue;
+ }
+
+ EvaluatePath(redirect.Target);
+}
+```
+
+PowerShell streams 3-6 and `*>` currently map lossily onto the shared redirect
+enum. The target remains available for path policy, but consumers must not use
+`RedirectDirection` to recover the exact original PowerShell stream.
+
+## Compounds, pipelines, and wrapped commands
+
+`ParsedCommand.Clauses` is ordered. Each clause carries the operator that
+preceded it:
+
+- `AndIf`, `OrIf`, and `Sequence` normally introduce a new statement;
+- `Pipe` connects pipeline stages;
+- `None` marks the first clause.
+
+A UI may group a pipeline as one approval prompt, but authorization should
+still inspect every stage. `download | sh` is unsafe even if `download` alone
+is allowed.
+
+ShellSyntaxTree also looks through supported command-string wrappers. Clauses
+surfaced from `bash -c`, `pwsh -Command`, and `pwsh -EncodedCommand` carry
+`IsCommandStringWrapped = true`. The outer wrapper is not the action a
+verb-based policy should authorize; the surfaced inner clauses are.
+
+PowerShell script blocks, subexpressions, splats, and `--%` regions are opaque
+and surface as `DynamicSkip`. A dynamically invoked command such as `& $exe`
+sets `VerbChain.IsDynamic = true`; no verb-pattern grant should match it.
+
+## Safe-fail rules
+
+For a security gate, these conditions should prevent a durable automatic
+grant:
+
+- `ParsedCommand.IsUnparseable` is true;
+- the non-empty input produces no clauses;
+- a clause has `Verb.IsDynamic` or no statically known command identity;
+- a dynamic argument or redirect affects a policy-sensitive position;
+- a future package version introduces an enum or AST shape the consumer has
+ not mapped.
+
+The recoverable outcome is normally a user prompt with a one-time option, or a
+deny. A false-negative approval match causes another prompt; a false-positive
+match can silently execute something the operator did not authorize.
+
+## Worked use cases
+
+### AI-agent approval gate
+
+Input:
+
+```text
+cd /repo && rm -rf build
+```
+
+The consumer can derive:
+
+- two clauses joined by `AndIf`;
+- action `rm` with flags `-rf`;
+- explicit path `build`, resolved beneath `/repo`;
+- inherited cwd `/repo` on the `rm` clause.
+
+The product can allow deletion under a disposable build directory, prompt for
+an unfamiliar workspace, and deny protected system zones.
+
+### CI/CD auditor
+
+Input:
+
+```text
+dotnet test > /tmp/test.log && curl https://example.invalid/install | bash
+```
+
+The auditor can inspect the redirect path, split the second statement from the
+first, recognize the pipeline, and warn that downloaded content is piped into a
+shell.
+
+### Sandbox planner
+
+Input:
+
+```text
+Set-Location C:\src; Copy-Item .\out\app.dll C:\deploy\app.dll
+```
+
+The consumer can collect the attributed cwd and both path operands to propose
+read/write mounts. It should still apply its own cmdlet policy and access-mode
+rules; ShellSyntaxTree reports paths, not filesystem permissions.
+
+### Command-aware policy
+
+Input:
+
+```text
+git -C /repo commit
+git commit -C HEAD~1
+```
+
+These commands demonstrate why source provenance matters. Git assigns different
+meaning to `-C` based on whether it appears before or after `commit`.
+[Issue #62](https://github.com/Aaronontheweb/ShellSyntaxTree/issues/62)
+tracks an ordered clause-element API so a Git-aware consumer can apply that
+rule without re-tokenizing `ParsedCommand.Source`. Until that API ships, the
+current `Verb` and `Args` projections do not preserve their interleaving.
+
+When ordered elements are added, this guide should be updated in the same
+change with a complete command-aware-policy example. The existing projections
+should remain documented as compatibility conveniences, while the ordered view
+becomes the source-provenance path for consumers that need positional meaning.
+
+## Netclaw case study
+
+[Netclaw](https://github.com/netclaw-dev/netclaw) is ShellSyntaxTree's original
+consumer. Its approval gate is a useful production case study, but its policy
+choices are not part of ShellSyntaxTree's contract.
+
+The links below are immutable references to Netclaw commit
+[`74014139a833050d777fbc913345904cca3b0544`](https://github.com/netclaw-dev/netclaw/commit/74014139a833050d777fbc913345904cca3b0544):
+
+- [Package reference and version pin](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/Netclaw.Security.csproj#L9-L12)
+ show the consumer dependency. That snapshot uses ShellSyntaxTree 0.1.5 and
+ therefore demonstrates the POSIX/Bash integration, not the newer PowerShell
+ parser.
+- [Dependency-injection registration](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/SecurityServiceExtensions.cs#L33-L42)
+ binds `IShellParser` to `BashParser`.
+- [Parser construction and safe-fail adaptation](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/IToolApprovalMatcher.cs#L200-L223)
+ supply the invocation working directory and convert unparseable or empty
+ results into "cannot decompose."
+- [Approval-candidate extraction](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/IToolApprovalMatcher.cs#L225-L265)
+ evaluates every clause and keeps Netclaw's command-specific normalization in
+ the consumer.
+- [Directory and redirect attribution](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/IToolApprovalMatcher.cs#L267-L345)
+ combine explicit operands, inherited cwd, and redirect targets.
+- [Approval-unit grouping](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/IToolApprovalMatcher.cs#L347-L405)
+ starts new units for statements while retaining pipeline stages together for
+ display.
+- [User-facing reconstruction](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/IToolApprovalMatcher.cs#L407-L465)
+ drops synthetic cwd attribution and applies product-specific summarization.
+- [Fail-closed authorization](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/IToolApprovalMatcher.cs#L549-L576)
+ refuses automatic approval when the parser cannot produce candidates.
+- [Package integration canaries](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security.Tests/ShellSyntaxTreeIntegrationTests.cs#L14-L182)
+ pin parser registration, verb extraction, compound splitting, cwd
+ attribution, and dynamic-content behavior across package upgrades.
+
+The reusable lesson is the flow: parse with real context, fail safely, inspect
+every clause, keep derived cwd separate from authored tokens, and layer
+application policy over parser facts. Netclaw's verb trimming, side-effect
+classification, path predicate, and approval persistence are intentionally
+application-specific.
+
+## Runnable samples
+
+The repository includes two public samples:
+
+- [`ShellSyntaxTree.Cli.Sample`](../samples/ShellSyntaxTree.Cli.Sample/) prints
+ the AST and applies a deliberately small audit policy.
+- [`ShellSyntaxTree.Web.Sample`](../samples/ShellSyntaxTree.Web.Sample/)
+ renders Bash and PowerShell parses as Mermaid diagrams in the browser.
+
+The CLI policy is an illustration, not a production allow-list. It is useful
+for seeing how a consumer walks paths, dynamic arguments, redirects, and
+adjacent pipeline clauses. The Netclaw links above show how those primitives
+fit into a real approval lifecycle.
+
+## Related contracts
+
+- [`SPEC.md`](../SPEC.md) defines the shared AST and Bash behavior.
+- [`SPEC.POWERSHELL.md`](../SPEC.POWERSHELL.md) defines PowerShell-specific
+ parsing, alias resolution, parameter binding, and resolver behavior.
+- [`README.md`](../README.md) provides installation and quick-start examples.