How scripts work

A script turns a check like cargo test into a step the agent must pass through, not a suggestion it can skip — and pins the body so it can't drift. Inline body or external uri+hash, invocable from any workflow. Here's the operational rundown.

A script is how you make cargo test a step the agent must pass through, not a suggestion it can skip — hash-pinned so the body can’t drift. Where skills tell the model what to think, scripts tell the workflow what to do. Deterministically. Same shape conventions — verb, subject, lifecycle, body — but a fundamentally different runtime contract: the script executes, not the model.

Script verbs covers the 12-verb taxonomy. This page is the operational rundown — where scripts live, how the body gets materialized, how a workflow invokes them, and the hash-pinning + snapshot invariant.

Anatomy

A script entry under the top-level scripts: block (or in a repo’s scripts-library/ directory):

scripts:
  test.cargo.workspace:
    verb: test
    lifecycle: stable
    source: my-team
    body: |
      #!/usr/bin/env bash
      set -euo pipefail
      cargo test --workspace --no-fail-fast "$@"
Field Required What it does
verb yes One of the 12 script verbs (reference). Closed enum.
lifecycle yes experimental | stable | deprecated. Same conventions as skills.
body xor with uri Inline script. Materialized to a chmod 0700 tempfile at execute time.
uri xor with body External script source. file://, https://, or git+https://...@<ref>#<path>.
hash required iff uri sha256:<64 hex chars>. The runtime fetches the URI, hashes the body, and refuses to execute if it doesn’t match.
source no Provenance string (my-team, cognitive-architectures, git+https://...). Surfaced in audit.

The subject (the key — test.cargo.workspace) is a dotted namespace, same shape as skills. First segment must be a blessed root from the script subject roots; build, test, deploy, format, lint, install, verify, run, inspect, search, fetch, audit.

Inline vs external — pick the right one

The two body sources solve different problems.

Inline body:

scripts:
  format.rust.check:
    verb: format
    lifecycle: stable
    body: |
      #!/usr/bin/env bash
      set -euo pipefail
      cargo fmt --all -- --check

Self-contained. The script body lives in the YAML; no external fetch at load time; no operator-side filesystem assumption beyond “bash is available.” Good for small portable scripts every operator can run identically.

External uri: + required hash:

scripts:
  deploy.cargo-install:
    verb: deploy
    lifecycle: stable
    uri: "file://scripts-library/deploy.cargo-install.sh"
    hash: "sha256:c9f4b8...e2a"

The script body lives in an external file (or HTTP URL, or git ref). The runtime fetches it at load time, hashes the body, and aborts if the hash doesn’t match. The hash is the integrity gate — a hijacked endpoint, a corrupted file, or a typo in the script content all surface as SCRIPT_HASH_MISMATCH.

Use external uri: when:

Supported URI schemes:

Scheme Resolution When to use
file://path/relative.sh Resolved at load time against the YAML file’s directory. Becomes absolute before runtime sees it. Scripts that ship in the same repo as the gateway config. The common case.
file:///absolute/path Already absolute. Scripts installed system-wide.
https://... Blocking HTTP GET at load time. 30-second timeout. Centrally-hosted script libraries. The hash is what makes the network call safe.
git+https://<host>/<repo>(.git)?@<ref>#<path> git archive --remote=<url> <ref> <path> at load time. Scripts pinned to a specific git ref (SHA or signed tag). Many forges (GitHub, GitLab.com) disable upload-archive over HTTPS for security; if yours does, host the file via raw HTTPS or run a local mirror.

Inline + hash: is also legal: the hash is recomputed and verified at load. Useful for catching accidental edits to inline scripts before they ship.

Where scripts live

Same two-place model as skills.

Inline in the gateway config

# gateway.yaml
version: "1.0.0"

scripts:
  test.cargo.workspace:
    verb: test
    lifecycle: stable
    body: |
      #!/usr/bin/env bash
      set -euo pipefail
      cargo test --workspace "$@"

Simplest path. One file, no repo manifest.

From a repo’s scripts-library/ directory

A resource repo declares scripts under its manifest’s scripts: layout key (defaults to scripts-library/):

# repos/cognitive-architectures/praxec.repo.yaml
schema: praxec.repo/v1
namespace: cognitive
layout:
  scripts: scripts-library     # ← scripts-library/*.yaml load from here
# repos/cognitive-architectures/scripts-library/test.cargo.workspace.yaml
scripts:
  test.cargo.workspace:
    verb: test
    lifecycle: stable
    body: |
      #!/usr/bin/env bash
      set -euo pipefail
      cargo test --workspace "$@"

Loaded scripts get namespace-prefixed cognitive/test.cargo.workspace. Same disambiguation rules as skills — two repos can ship the same bare subject without colliding.

How a workflow invokes a script

Via the script executor kind:

workflows:
  verify-pipeline:
    initialState: verifying
    states:
      verifying:
        transitions:
          run:
            target: done
            actor: deterministic
            executor:
              kind: script
              subject: test.cargo.workspace
            output:
              passed: "$.output.passed"

The runtime:

  1. Looks up test.cargo.workspace in the workflow’s stamped _scriptsLibrary snapshot (see below).
  2. Materializes the body to a chmod 0700 tempfile.
  3. Executes via shebang line (or bash fallback).
  4. Captures stdout / stderr / exit code.
  5. Emits a script_output evidence record with the body hash for audit replay.
  6. Surfaces the parsed stdout as the executor’s output for the transition’s output: mapping.

For cross-namespace script references from a repo-loaded workflow, the same fall-through resolution applies as for skills:

  1. Try the workflow’s own namespace prefix first (cognitive/test.cargo.workspace).
  2. Then the bare subject (test.cargo.workspace).

Fully-qualify to reference scripts from a different namespace explicitly (internal/deploy.kubectl-strict).

Snapshot stamping (_scriptsLibrary)

Same SPEC §8.2 invariant as _skillsLibrary. When praxec.command({definitionId}) creates an instance, the runtime stamps a _scriptsLibrary onto the instance’s definition snapshot — copy of every script the workflow references, with body, hash, and verb included.

For inline body: scripts the snapshot carries the literal text. For external uri: scripts the snapshot carries the resolved body — the runtime fetched it at load time, verified the hash, and snapshotted the verified bytes. The instance executes against the snapshotted body, not against a re-fetch.

What this buys:

The cost is the same as skills’ snapshot stamping — the script body lives in the instance’s snapshot. For typical 10–100-line scripts, this is negligible.

Hash normalization

Script hashing collapses trailing newlines but preserves all internal whitespace exactly. Shell scripts treat whitespace as load-bearing (if [[ $x == "y" ]] vs if [[ $x == "y" ]] are different programs); the normalization is stricter than skills’.

The rule:

If you compute a hash for a uri: script and the runtime rejects with SCRIPT_HASH_MISMATCH, the most common cause is editor-inserted trailing whitespace or a tab/space mix you didn’t notice. Use sha256sum on the bytes you want hashed; the runtime uses the same normalization function for both load-time verification and the snapshot-stamp hash.

file:// path resolution

file://path/script.sh is relative to the YAML file that declares it. The loader rewrites the URI to an absolute path before any downstream code sees it. A repo at ~/repos/swe-core/ with praxec.repo.yaml referencing file://scripts-library/deploy.sh resolves to ~/repos/swe-core/scripts-library/deploy.sh.

This means moving a repo around the filesystem doesn’t break its scripts — relative paths follow the repo. It also means a config that uses file:// URIs is portable across operators as long as the relative layout is the same.

For absolute paths use file:///etc/scripts/deploy.sh (three slashes). The loader leaves absolute paths alone.

Authoring discipline

What’s next