Manifest Command Trust
August 29, 2026 ยท View on GitHub
Base project manifests can declare commands that run project-owned shell code. Those declarations are useful because they make project workflows discoverable, but they also create a local trust boundary: cloning or pulling a repository should not by itself imply consent to execute that repository's commands.
This document defines the allow model for manifest-declared command execution.
Goals
- Require explicit local approval before first execution of manifest-declared project code from an unfamiliar repository.
- Keep read-only inspection paths available before approval.
- Re-require approval when the manifest command contract changes.
- Store approval in Base-managed local state, not in the project repository.
- Behave predictably in non-interactive shells and CI.
Non-Goals
- Do not sandbox project commands.
- Do not claim that linting or approval makes hostile project code safe.
- Do not block read-only manifest validation or workspace reporting.
- Do not make workspace clone or workspace pull imply command execution trust.
Commands Covered
Base requires an allow record before commands that execute manifest-declared project code:
basectl test [project]basectl run [project] <command>basectl build [project] [target...]basectl demo [project]basectl activate <project>when it will sourceactivate.sourceentries
These paths remain allowed before approval because they inspect or report without executing project-owned shell code:
basectl projects listbasectl workspace status,check, anddoctorbasectl run [project] --listbasectl build [project] --listbasectl test [project] --dry-runbasectl run [project] <command> --dry-runbasectl build [project] [target...] --dry-runbasectl demo [project] --dry-runbasectl check [project]andbasectl doctor [project]basectl export-context [project]
Setup delegates such as Brewfiles, mise install, and artifacts are separate
trust boundaries. Project-originated IDE mutations have their own explicit
approval boundary: basectl setup --dry-run shows the complete app, extension,
and user-settings plan, and applying that plan requires
--allow-project-ide-mutations. --yes never supplies this approval. This
keeps command execution trust and machine-wide IDE mutation consent separate.
Trust Identity
An allow record binds approval to both repository identity and the manifest command contract. The identity includes:
- canonical project root path
- canonical manifest path
- raw SHA-256 digest of
base_manifest.yaml - project name from the manifest
- Git repository root when available
- sanitized
originremote URL when available - current Git HEAD when available, for display and audit metadata
The enforcement key includes the canonical project root, manifest path, and manifest digest. The remote URL and Git HEAD are stored for display and audit, but the manifest digest is the only repository input that forces re-approval after command declarations change. Status, allow, and blocked-command output prints the following warning so the boundary is visible:
Approval is bound to the
base_manifest.yamlSHA-256 only. Manifest changes invalidate approval; referenced scripts, direct executable files, Git HEAD, and uncommitted working-tree changes are not independently verified or bound to approval. Re-review those inputs before execution after repository changes.
This intentionally resembles direnv allow: approval is local to the machine
and is invalidated by a content change in the trusted file. It does not try to
prove that every script reachable from the manifest is unchanged.
Local State
Base stores allow records under Base-managed local state:
~/.base.d/trust/manifest-commands/
<identity-key>.json
Each record uses schema version 1:
{
"schema_version": 1,
"allowed_at": "2026-07-03T12:00:00Z",
"allowed_by": "local-user",
"base_version": "1.5.0",
"project": {
"name": "demo",
"root": "/Users/rameshhp/work/demo",
"manifest": "/Users/rameshhp/work/demo/base_manifest.yaml",
"manifest_sha256": "<sha256>",
"git_root": "/Users/rameshhp/work/demo",
"origin": "https://github.com/example/demo.git",
"head": "<commit-sha>"
},
"allowed_commands": ["test", "run", "build", "demo", "activate"]
}
Writes should be atomic: write a temporary file in the same directory, then rename it into place. Base should never write allow records into project repositories.
User-Facing Flow
Use the focused trust command to inspect, allow, or revoke approval:
basectl trust status [project] [--workspace <path>] [--format text|json]
basectl trust allow <project> [--workspace <path>] [--manifest-sha256 <sha256>]
basectl trust revoke <project> [--workspace <path>]
With a project, trust status reports that one manifest. Without a project, it
reports every discovered project whose manifest declares an executable test,
run, build, demo, or activation surface. Metadata-only manifests are omitted so
workspace onboarding does not suggest unnecessary approval.
For schema-v1 compatibility, explicit single-project JSON continues to report
the underlying allow-record state, including status: blocked and an
allow_command for a metadata-only manifest. Human text reports that approval
is not required, and the project-less workspace view omits that manifest. The
existing status, reason, and conditional allow_command/record meanings
are unchanged.
basectl trust allow prints the exact identity being approved and requires the
supplied --manifest-sha256 to match when that option is present. That flag is
useful for scripted, non-interactive approval after a prior review step.
When execution is blocked, commands fail before project environment activation and before changing into project directories:
ERROR: Manifest-declared commands are not allowed for project 'demo' on this machine.
Project root: /Users/rameshhp/work/demo
Manifest: /Users/rameshhp/work/demo/base_manifest.yaml
Manifest SHA-256: <sha256>
Origin: https://github.com/example/demo.git
Review first:
basectl run demo --list
basectl build demo --list
basectl test demo --dry-run
basectl demo demo --dry-run
Inspect activate.source entries in /Users/rameshhp/work/demo/base_manifest.yaml before running 'basectl activate demo'.
Allow after review:
basectl trust allow demo --manifest-sha256 <sha256>
If a record exists for the same project root but a different manifest digest, the error says the manifest command contract changed and shows both the recorded and current digest.
Non-Interactive And CI Behavior
Manifest command execution must fail closed in non-interactive shells when no
matching allow record exists. It should not prompt, wait for input, or infer
trust from CI=true.
CI jobs that intentionally execute manifest commands should add an explicit allow step before execution:
basectl trust allow base-demo --manifest-sha256 "$EXPECTED_BASE_MANIFEST_SHA256"
basectl test base-demo
If a workflow cannot precompute the digest, it may run basectl trust status base-demo --format json first, review the emitted manifest_sha256 in a prior
step, and then call trust allow with that digest.
basectl trust status <project> --format json returns a structured payload:
{
"schema_version": 1,
"status": "blocked",
"reason": "not_allowed",
"project": {
"name": "demo",
"root": "/Users/rameshhp/work/demo",
"manifest": "/Users/rameshhp/work/demo/base_manifest.yaml",
"manifest_sha256": "<sha256>",
"origin": "https://github.com/example/demo.git",
"head": "<commit-sha>"
},
"trust_scope": {
"approval_basis": "base_manifest.yaml_sha256",
"manifest_changes_invalidate": true,
"referenced_script_changes_invalidate": false,
"direct_executable_file_changes_invalidate": false,
"git_head_changes_invalidate": false,
"working_tree_changes_invalidate": false
},
"allow_command": "basectl trust allow demo --manifest-sha256 <sha256>"
}
The project-less workspace form preserves each project payload under a versioned collection:
{
"schema_version": 1,
"projects": [
{
"schema_version": 1,
"status": "blocked",
"reason": "not_allowed",
"project": {
"name": "demo",
"manifest_sha256": "<sha256>"
},
"allow_command": "basectl trust allow demo --manifest-sha256 <sha256>"
}
]
}
Execution commands print the text block above and exit non-zero; JSON remains on
basectl trust status --format json.
Implementation Slices
- #1381: add trust identity
computation, local JSON storage, and
basectl trust status|allow|revokewith Python unit tests. - #1382: add Bash
enforcement before
test,run,build,demo, and activation source execution. Preserve--dry-runand--listinspection paths. - #1973: require explicit consent for project-originated IDE app, extension, and user-setting mutations; show the complete dry-run plan; and document that command trust is bound to the manifest digest only.