Skip to content
Back to skills

Unattended

BSecurity

Author an unattended PowerShell script for work that is fully scriptable but not agent-launchable: the human is only the privilege or policy boundary (UAC, an elevated shell, or a run the repo reserves for the operator). The agent authors the script and never launches the real run. The human launches it once. The script writes cutover.result/1 JSON the agent reads back. Use when: 'run this elevated', 'I have to launch it', 'UAC', 'operator must apply', 'unattended cutover', 'scriptable but I ...

  • 20 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 29, 2026
ai-agentsrustgoshellgitapisecurity

Works with

  • terminal
  • cli
  • api

Security analysis

B75/100
  • criticalAccesses system keychains or credential stores

Pro scans all 4 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add melodic-software/claude-code-plugins --skill unattended --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Unattended?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Unattended
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/melodic-software-unattended/badge)](https://www.skillsdirectory.com/skills/melodic-software-unattended)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
description: "Author an unattended PowerShell script for work that is fully scriptable but not agent-launchable: the human is only the privilege or policy boundary (UAC, an elevated shell, or a run the repo reserves for the operator). The agent authors the script and never launches the real run. The human launches it once. The script writes cutover.result/1 JSON the agent reads back. Use when: 'run this elevated', 'I have to launch it', 'UAC', 'operator must apply', 'unattended cutover', 'scriptable but I cannot run it'. Don't use it for a dashboard click, a 2FA code, or anything the agent can already run itself."
argument-hint: "<scriptable procedure a human must launch>"
user-invocable: true
disable-model-invocation: false
metadata:
  workflow-stage: anytime
  summary: Author an unattended script a human launches once for a privilege or policy boundary
---

# Unattended launch

`/wizard:generate` is for a step a human has to perform: a dashboard click, a
2FA code, an unplug. This skill is the other case. The work is fully
scriptable, and a human is in the loop only because the agent lacks the
privilege or a policy reserves the run for the operator. The human launches
the script once. The script does the work and writes a result file. The agent
reads that file. The human does not paste a terminal log back into the chat.

An irreversible step (`wsl --unregister`, `git push --force`) still asks the
human, in this skill or in `generate`. That is a consent prompt, not a reason
to make the rest of the script interactive.

## When to use which

| Reason a human is involved | Owner |
|---|---|
| Agent lacks the privilege (UAC, elevated shell) | this skill |
| Agent is forbidden by policy | this skill |
| Dashboard, 2FA, or another step no script can perform | `/wizard:generate` |
| Irreversible and needs consent | `Confirm-Irreversible` in the script |

## Process

### 1. Scope

List the steps, the inputs, and why the agent cannot launch the script. Read
the repo for commands and key names. From a live `.env`, take key names only.
Never values. Mark each irreversible step with the exact name the human will
type (`wsl --unregister Ubuntu-26.04`); step 2 passes those names to
`-Irreversible`. Show the ordered stages and wait for the user to confirm them.

**Done when:** every stage is named, each input has a resolution rung, and
each irreversible step is marked and listed as an `-Irreversible` name.

### 2. Author

Copy [template.ps1](template.ps1). Leave the library above `# STAGES` unchanged.
Replace the example `Invoke-UnattendedRun` body with the procedure, using only
these helpers:

- `Invoke-UnattendedRun -ResultDirectory -Stages -Irreversible <names> -Secrets <declarations>`.
  Runs the stages and writes the result. `-Irreversible` is the declared list of
  irreversible steps, for example `'wsl --unregister Ubuntu-26.04'`. It is
  printed to the transcript before the stages run (`irreversible actions:
  none` when empty; not under `-Test`) and recorded as `irreversible_actions`,
  on success and on failure. `-Secrets` is the declared list of secrets, each
  `@{ Name = 'API_TOKEN'; FilePath = 'C:\ops\token.txt' }` (`FilePath` is
  optional). Every declared name resolves once, before the first stage, so the
  human answers every hidden prompt up front and the rest of the run is
  unattended. The names are printed (`secrets: none` when empty; not under
  `-Test`) and recorded as `secrets`, names only, on success and on failure.
- `Assert-Elevation -Mode Required` or `Forbidden`. Elevation is a constraint
  with two failure directions.
- `Assert-NotInside -Name <wsl-distro>`. The script must not be running inside
  the WSL distro it restarts. It compares `Name` with `WSL_DISTRO_NAME` only: a
  service, container or process is not detected. A WSL login cutover cannot be a
  script running inside that distro; emit PowerShell so it runs on the Windows
  host.
- `Resolve-UnattendedSecret -Name <ENV> -FilePath <optional>`. First hit wins:
  environment variable, then the file, then a `Microsoft.PowerShell.SecretManagement`
  vault, then the native store (macOS Keychain through `security
  find-generic-password -s <name> -w`; Linux `pass show <name>` for an entry file `<name>.gpg`, first line
  only), then one hidden prompt. Each store rung is skipped silently when its
  module, command or the name is absent. The vault uses only a string secret
  (see the store gotchas). A name declared in
  `-Secrets` returns the value resolved before the first stage; declaring a name
  twice fails the run. An undeclared name runs the ladder at this call, with its
  own prompt. The value is redacted out of the transcript.
- `Assert-PriorResult -Path <result-latest.json>`. Do not start until the
  previous script's result is `ok`.
- `Add-Preflight -Name -Test -Fix`. Fail before later steps, and carry the
  remediation command in the result.
- `Invoke-IdempotentStep -Name -Done -Action`. A re-run after a partial failure
  skips work that is already done.
- `Wait-ForState -Name -Predicate -TimeoutSeconds 300 -IntervalSeconds 5`. The
  preferred way to prove a requested state: poll the outcome, never the exit
  code of the request. It polls the predicate, prints one progress line per
  poll, and returns once the predicate's last output is truthy; otherwise it
  throws `timed out waiting for <Name>` with the last value or error. A
  predicate that throws counts as not yet, because ephemeral targets race. The
  predicate must first assert a non-empty observation (`$pools.Count -gt 0 -and
  ...`): a vacuous pass is the author's bug, so a drain proof that sees no pools
  must fail, not pass. It records a `wait <Name>` step. It emits `$true`, so
  standing alone it is written `$null = Wait-ForState ...`.
- `Use-GuardedResource -Name -Take -Prove -Release [-TolerateTakeExit]`. Take a
  shared resource out of service and release it only after proof. `Prove` must
  throw on failure or emit a truthy value as its last output; `$false`, no
  output, or a nonzero native exit all count as failed proof and keep the
  resource held. A failure leaves it listed in `held_resources`.
  `-TolerateTakeExit` records a nonzero native exit from `Take` as a warning
  instead of throwing (a thrown exception still fails), so `Prove` is the only
  gate. Drain example: the request exits 5 while the drain proceeds, so use
  `-TolerateTakeExit` with `-Prove { Wait-ForState -Name drain -Predicate {
  $pools.Count -gt 0 -and -not ($pools | Where-Object Active) } }`.
- `Assert-ParsedState -Name -Value`. Throws when a parsed listing is `$null`, an
  empty array, or only whitespace, and otherwise prints the item count.
  Unknown state is a stop, not `already absent`.
- `Invoke-NativeUtf8 -Block`. Runs the block with `WSL_UTF8=1` and UTF-8
  console decoding, restores both afterwards (also when the block throws), and
  returns the block's output. It is the pin for `wsl.exe` listings (see
  Gotchas). A CLI that has `--json` should use that instead of parsing human
  output.
- `Confirm-Irreversible -Name`. The human types the name. Anything else aborts.
  It refuses before prompting when the name is not in `-Irreversible`
  (`irreversible step X was not declared`) and while any guarded resource is
  still held (`refusing irreversible step X while resources are held: ...`).
  A confirmed step is recorded as `irreversible <Name>`.

Set the result directory to a path the agent can read after the human runs the
script. The envelope is `cutover.result/1`: per-step `status` and `detail`,
`mode`, `warnings`, `held_resources`, `irreversible_actions`, `secrets`,
`transcript`, and a `result-latest.json` copy. The schema string is unchanged,
and `irreversible_actions`, `secrets` and `mode` are additive fields: read their
absence in an older result as an empty list and `run`.

#### Order and unknowns

- Irreversible steps go last, after every precondition is proven in the same
  run. A proof from an earlier run or an answer typed at a prompt is not one.
  `Confirm-Irreversible` enforces the resource half: it refuses while any
  `Use-GuardedResource` is still held.
- Every guard whose false branch skips a destructive step
  (`-Done { $names -notcontains 'Ubuntu-26.04' }`) reads a listing that is empty
  when the read failed. Pass the parsed listing through `Assert-ParsedState`
  first, so an unknown state stops the run instead of reading as `already
  absent`.
- After the destructive step, re-verify the outcome with `Wait-ForState` (the
  distro is gone, the port is closed). The destructive command's exit code is
  not that proof.

#### Dry run

The script takes `-WhatIf` and `-Test`. Neither invokes a mutating block, and
both write only the result directory, as `result-dry-latest.json`, so a
preview never replaces a real run's `result-latest.json`.

- `-WhatIf` narrates the plan: one `What if:` line per step, including steps
  already done, then the blast radius as counts: steps that would run,
  resources that would be taken out of service, declared irreversible actions.
- `-Test` reports the delta: nothing is printed but one final line. The result's
  `delta` array holds only the steps that would run plus failed preflights.
  With both switches, `-Test` wins.
- The result gains `mode` (`run`, `whatif`, `test`) and, in a dry run,
  `planned` with the counts `steps`, `resources`, `irreversible` and `secrets`.

Read-only helpers run in every mode, so a missing prerequisite fails a dry run
with no side effects: `Assert-Elevation`, `Assert-NotInside`,
`Assert-PriorResult`, `Add-Preflight`, `Assert-ParsedState`,
`Invoke-NativeUtf8`, the `-Done` probe of `Invoke-IdempotentStep`, and the
environment, file and store rungs of secret resolution (vault, Keychain, `pass`). Each
probe, preflight test and wrapped read must only read. The vault rung reads
every registered vault, and a locked vault, Keychain or `pass` can ask the human
for a password during a dry run (see the store gotchas).

Mutating helpers skip their blocks and record a `would-run` step, or `skipped`
when `-Done` is already true: `Invoke-IdempotentStep -Action`,
`Use-GuardedResource` (Take, Prove and Release), `Confirm-Irreversible` (no
prompt), `Wait-ForState` (no polling), and secret resolution, declared or not (no
hidden prompt for a secret; each name that the environment, the file and every store
miss records a `would prompt` step and yields the placeholder `<NAME>`).

A dry run does not exercise success detection inside a step: no Prove block or
`Wait-ForState` predicate runs, so it checks the plan and the prerequisites, not
that the state is reached, and it does not replace `Wait-ForState`. Put every
effect inside a helper's block: a bare native command in `-Stages` runs in every
mode.

### 3. Hand off

Do not launch the script. Print the `# STAGES` block and get explicit approval
first: the agent runs nothing before the human has seen the stages. Check that
every effect sits inside a helper's block, because `-Test` runs a bare native
command. After approval the agent's own entry is `pwsh -File <script> -Test`,
which changes nothing when every effect is inside a helper: read
`result-dry-latest.json` (`status`, `delta`, `planned`) and fix what it
reports; if a fix changes the stages, print them and get approval again. Then
tell the human to launch it. Say which elevation mode it demands, that it requires PowerShell 7 (`pwsh`),
launched with `pwsh -File <script>` (Windows PowerShell 5.1 fails at
`#requires`), and the result path. Tell the human to run `-WhatIf` first, read
its narration and blast radius, and only then make the real launch. After the
real run, read `result-latest.json`. Do not ask them to paste the transcript.

## Next

`/wizard:generate` when a step is a dashboard click, a one-time code, or another action no script can perform.

## Gotchas

- `Assert-NotInside` reads `WSL_DISTRO_NAME`. `WIZARD_INSIDE_MARKER`, when set,
  replaces it: that is the test seam `template.test.sh` uses, not an operator
  setting. A script running inside a service or container passes the guard.
- The agent never launches the real run. A pipeline or an agent shell is the
  wrong principal. `-Test` is the one exception, because it invokes no mutating
  block. It also sets `-WhatIf`, so a cmdlet outside a helper that honors
  `-WhatIf` is skipped too; a native command outside a helper is not.
- Dry-run behavior worth knowing: a standalone `Wait-ForState` is not polled,
  because the state it waits for follows a mutation the dry run skipped;
  `Use-GuardedResource` never lists the resource
  in `held_resources`, because nothing was taken; `Resolve-UnattendedSecret`
  and a declared secret return the placeholder `<NAME>` when they would prompt;
  `Confirm-Irreversible`
  still refuses an undeclared name, before it would prompt. A `-Done` probe that
  throws fails a dry run as it fails a real one, so write probes that tolerate a
  target that does not exist yet.
- `-Test` from the agent's own shell stops at `Assert-Elevation -Mode Required`
  with status `failed` and `refusing to run unelevated`: the shell lacks the
  privilege, and that is the expected result. The human's elevated `-WhatIf`
  reaches the rest of the script.
- A `Take` that asks a system to reach a state (a drain with `--wait`) can exit
  nonzero while the state is reached. Do not trust that exit code either way:
  pass `-TolerateTakeExit` and prove the state with `Wait-ForState`.
- `wsl.exe` writes its listings (`--list`, `--status`) as UTF-16, which a
  PowerShell capture decodes as text with embedded NULs, so a `-contains
  'Ubuntu'` guard never matches and reads `absent`. `WSL_UTF8=1` makes it emit
  UTF-8 (WSL 0.64.0 and later); wrap the call in `Invoke-NativeUtf8`. Verified
  2026-09-29 against the 0.64.0 release notes
  (<https://github.com/microsoft/WSL/releases/tag/0.64.0>), `WslClient.cpp` in
  `microsoft/WSL` (reads `WSL_UTF8`; only the value `1` enables UTF-8), and
  Microsoft's `diagnostics/collect-wsl-logs.ps1` (sets it beside
  `[Console]::OutputEncoding`). The Microsoft Learn WSL pages do not mention
  it. Recheck when a WSL release note changes or drops `WSL_UTF8`, or the Learn
  basic-commands page documents a different switch for `wsl.exe` output
  encoding.
- The store rung calls `Get-Secret -AsPlainText` with no `-Vault`, so it searches
  every registered vault, the default vault first and remote ones included, on any
  platform where the module and a vault are present. Nothing gates it to Windows. A
  locked SecretStore whose `Interaction` is `Prompt` asks for its password in an
  interactive session, so `-WhatIf` and `-Test` can prompt for it too; with
  `Interaction` set to `None` the read fails and the rung is skipped.
  `-AsPlainText` converts only a `String` or `SecureString`, so a `PSCredential`,
  hashtable or `byte[]` secret is skipped with a warning and the ladder goes on
  to the hidden prompt. SecretStore encrypts with .NET Core cryptographic APIs,
  not DPAPI. Verified 2026-09-29 against the SecretManagement
  [overview](https://learn.microsoft.com/en-us/powershell/utility-modules/secretmanagement/overview)
  (SecretStore "uses .NET Core cryptographic APIs to encrypt file contents" and
  "works on all platforms that support PowerShell 7"; the modules are "feature
  complete" with the repository archived, at SecretManagement 1.1.2 and
  SecretStore 1.0.6),
  [Get-Secret](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.secretmanagement/get-secret?view=ps-modules)
  (with no vault named, "all registered vaults are searched"; `-AsPlainText` "has
  no effect" on a secret that is not a `String` or `SecureString`) and
  [Set-SecretStoreConfiguration](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.secretstore/set-secretstoreconfiguration?view=ps-modules)
  (`-Interaction`). Recheck when the PowerShell Gallery lists a release of either
  module after those versions, or the overview page drops its "feature complete"
  notice.
- The native store rungs can block an unattended run. The Keychain can raise an
  access dialog for an item the calling app is not trusted for or in a locked
  keychain, and `pass` runs `gpg`, which asks for a passphrase through pinentry
  when the agent has none cached. Before an unattended run, unlock the keychain
  (`security unlock-keychain`) or add the calling app to the item's trusted
  applications, and start `gpg-agent` with the passphrase cached (for example by
  running `pass show <name>` once). `pass show` prints the whole file; the rung
  takes the first line. Verified 2026-09-30 against the Apple
  [`security` man page](https://keith.github.io/xcode-man-pages/security.1.html)
  (`find-generic-password`: `-s` matches the service string, `-w` displays the
  password only; `unlock-keychain`) and the passwordstore.org
  [pass man page](https://git.zx2c4.com/password-store/plain/man/pass.1) (`show`
  decrypts and prints the named password; `gpg-agent` is recommended so batch
  decryption needs less intervention). Recheck when either man page changes
  those flags or commands.
- A secret resolved at runtime stays in the human's process. Do not ask for
  the value in chat.
- `Confirm-Irreversible` is the consent prompt. Do not skip it because the
  rest of the script is unattended.
- Redaction runs when the script completes or throws. A run killed before
  that (Ctrl+C, a closed window, a reboot) leaves the transcript unredacted,
  so never print a secret, and tell the human to delete the transcript of an
  interrupted run.
- `Assert-PriorResult` trusts the file it reads. When a lower-privilege stage
  feeds an elevated one, put the result directory where only the elevated
  principal can write.

Files in this skill

  • SKILL.md4.8 KB
  • evals/evals.json2.5 KB
  • template.ps18 KB
  • template.test.sh7.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…