Introduction to Deputy
Secure dependency management at scale, from diff to fix.
The core idea
Dependencies are most of modern software. They live in repos, images, registries, and SBOMs, each with its own identity rules and edge cases. Most tooling solves a slice. You end up doing the reconciliation by hand.
Deputy flips that. It reconciles everything into a high-fidelity, inspectable dependency
inventory, and when the input has relationship data, a dependency graph on demand. Then it keeps
the rest of the workflow close to that foundation: scan for known issues, diff for reviews, sbom for sharing and archiving, graph why for paths, fix for reviewable remediation plans, exec for sandboxed installs and builds, and proxy for enforcement at download
time.
Deputy helps answer questions you need answered fast:
- What's in here? (repo, dir, image, Dockerfile, SBOM)
- What changed? (between two git refs, container images, etc)
- Why is it here? (the dependency path)
- Is it allowed? (policy, everywhere)
- What do we do next? (a reviewable remediation plan)
Policy keeps the guardrails consistent
Policies are authored in YAML using CEL, a small expression language for writing readable, deterministic checks over structured data. That keeps policies easy to review, and the exact same expressions run in local scans, CI, and proxy enforcement.
Check them into your repo, lint them in CI, and reuse the same bundle across scan, diff, and proxy. They can warn or deny. The point
is that the same rule shows up in review, in CI, and at download time. If you want to borrow
patterns, start with the policy examples or the policy cookbook for easy recipes.
Block copyleft licenses
A simple compliance guardrail: deny packages with disallowed licenses and flag missing metadata. Works anywhere policy is evaluated (scan, diff, proxy, or SBOM analysis).
policies: - name: allow-sans-copyleft vars: forbidden: - SSPL-1.0 - AGPL-3.0-only - GPL-3.0 rules: - action: deny when: pkg.licenses.exists(l, l in forbidden) reason: package carries a forbidden license
$ deputy scan --policy policy/examples/license-allowlist.yamlRequire review for new dependencies
PR guardrail: allow known prefixes and force review for anything else. This plugs straight
into deputy diff so reviewers see the policy decision next to the change.
policies: - name: new-dependency-review vars: approvedPrefixes: - github.com/acme - npm:@acme isAddition: 'change.type == "added"' name: 'pkg.name' matchesPrefix: 'approvedPrefixes.exists(p, name.lowerAscii().startsWith(p.lowerAscii()))' rules: - action: deny when: env.entrypoint == "diff_dependency_change" && isAddition && !matchesPrefix reason: New dependency requires review or is outside the approved allowlist
$ deputy diff --policy policy/examples/new-dependency-review.yaml main WORKING
Block high/critical vulns in CI
A hard gate for release pipelines: fail scans when critical or high vulnerabilities are still present. This is the classic “don’t ship with known fires burning” policy.
policies: - name: deny-critical-and-high entrypoints: ["scan_report"] rules: - action: deny when: | vulnerabilities.orValue([]).exists(v, v.advisory.severity.level in [severity.critical, severity.high]) reason: dependency has unresolved high-severity vulnerabilities
$ deputy scan --policy policy/examples/severity-guardrail.yamlGitHub Actions: policy gate in one job
Same policy bundle, now enforced in CI. Deputy’s GitHub Actions can scan on every push or PR and upload SARIF to Code Scanning. See the GitHub Actions guide for options.
name: Security Scan on: [push, pull_request] permissions: security-events: write contents: read jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: picatz/deputy/actions/setup@main - uses: picatz/deputy/actions/scan@main with: upload-sarif: true policy: policy/ci/security-gate.yaml
# Sanity check a policy bundle $ deputy policy lint policy/ci/security-gate.yaml # Enforce policy during scan $ deputy scan --policy policy/ci/security-gate.yaml # Require review for new dependencies in diffs $ deputy diff --policy policy/ci/pr-review.yaml main WORKING # Block risky downloads at install time $ deputy proxy go --policy policy/ci/security-gate.yaml -- go get github.com/example/pkg@latest
The command compass
Deputy has enough subcommands that it's worth a map. The full list is in the command docs.
Hover a command to preview a real invocation.
| Command | Purpose | Why it matters |
|---|---|---|
| Find known vulnerabilities across repos, dirs, images, VM images, SBOMs, and PURLs | Baseline risk and gate releases | |
| Explore dependency paths (e.g. why a package exists) | Answer "why is this here?" quickly | |
| Compare dependency changes between Git refs or container images | Review what actually changed | |
| Generate CycloneDX, SPDX, or Protobom SBOMs | Inventory you can share, sign, or archive | |
| List dependencies as PURLs for scripting | Fast inventory without a full SBOM | |
| Generate or apply remediation plans | Turn findings into action | |
| Prioritize findings (optional agent help) | Focus effort where it matters | |
| Run commands in a sandbox with supply chain protections | Safer installs and builds | |
| Enforce policies at download time | Block risky dependencies early | |
| Lint, test, bundle, and evaluate policies | Treat guardrails as code |
$ deputy scan --policy policy/ci/security-gate.yamlAdd --format json when you want machine-readable output.
Where to start
A few on-ramps, depending on what you're trying to solve:
- Developers / PR reviewers: start with
deputy diff. If something looks weird, follow withdeputy scananddeputy graph why. - Security: start with a policy bundle, run it in CI, then consider rolling out
deputy proxy. - Platform/CI: generate an SBOM with
deputy sbomand keep it as a build artifact, thenscanordiffit later. - New to Deputy: run
deputy initto generate a starter.deputy.yamland policy bundle, then iterate from there.
If you want the guided tour: getting started, the cheat sheet, workflows, and the CI guide.
Targets everywhere
Deputy commands take a target, meaning the thing you want to analyze. A target can be a repo, a directory, a container image, a VM disk image, an SBOM document, a Dockerfile, or a single package identified by a PURL.
Deputy inspects the target you pass in and applies the right semantics for that artifact. That
might mean extracting packages from a filesystem view, importing an SBOM document, or parsing a
Dockerfile, then applying the same policies to the results. For repos and directories, Deputy
can also resolve dependency relationships from manifests and lockfiles, which powers graph
questions like deputy graph why. When relationship data is missing or partial,
Deputy falls back to a flat inventory that is still useful for scans, diffs, and SBOMs.
SBOM files and one-off PURLs skip extraction and go straight to analysis. Dockerfiles are parsed for static dependency analysis rather than scanned like a filesystem. As Deputy grows, the intent is to keep this pipeline stable so new target kinds can slot in cleanly, including live targets like a running container or a cloud artifact like Lambda.
- Repositories (local or remote Git) and plain directories
- Container images (remote registries, local Docker daemon, tarballs)
- VM disk images (qcow2, vmdk, vhd, vhdx, vdi, raw) and raw filesystem images (ext4)
- SBOM files (CycloneDX / SPDX / Protobom JSON)
- PURLs for one-off package queries
- Dockerfiles for static dependency analysis
For VM disk images, Deputy stays unprivileged. It reads the disk image, parses the partition table (GPT or MBR), identifies a Linux partition, opens the filesystem (ext4), and walks it to build the inventory. No mounts and no root.
More details: targets and refs, container images, and Dockerfile scanning.
# Scan the current repo $ deputy scan # Scan a remote repo at a ref $ deputy scan github.com/picatz/deputy --ref main # Scan a container image $ deputy scan image nginx:1.25 # Scan a VM disk image $ deputy scan vm:///path/to/disk.qcow2 # Scan a rootfs filesystem image $ deputy scan rootfs:///path/to/rootfs.ext4 # Scan an SBOM file from another tool $ deputy scan sbom sbom.spdx.json # Scan a single package by PURL $ deputy scan purl pkg:golang/github.com/gin-gonic/gin@v1.9.0
$ deputy scan Scan Results: Target: /repo/deputy Ref: WORKING (b0f72a3) Origin: https://github.com/picatz/deputy.git ∴ Vulnerabilities Found: github.com/google/osv-scalibr v0.3.3 [direct]: • CVE-2025-13425 [LOW] (↑ 0.3.4) [2 related] OSV-SCALIBR has NULL Pointer Dereference Aliases: GHSA-f786-75f3-74xj, GO-2025-4149 Published: 2025-11-20 Context: Sources: • go.mod Artifacts: • deputy (go) Vulnerability Summary: ↑ 1 can be fixed by upgrading Recommended Actions: 1. Upgrade affected modules go.mod: › go get github.com/google/osv-scalibr@0.3.4 ↻ go mod tidy
When something pops up, the next question is always: "why is this here?" deputy graph why answers with the dependency path. More in the graph docs.
$ deputy graph why github.com/google/osv-scalibr github.com/google/osv-scalibr@0.3.3 [direct dependency]
deputy triage: turn findings into priorities
Scanning is cheap. Decision-making isn't. deputy triage helps you turn a raw report
into priorities you can act on, and it can optionally pull in an agent for extra context. The triage docs cover report formats and flags.
# Triage the current repo $ deputy triage # Delegate prioritization to an agent $ deputy triage --agent claude
deputy diff: where reviews get real
For PR review or base-image bumps, deputy diff answers the only question that matters:
"what actually changed?" It works on Git refs and container images, and it can include vulnerability
deltas and policy findings so you can spot regressions before you merge them.
# Git diff (default in a repo) $ deputy diff main WORKING # Compare container images $ deputy diff nginx:1.24 nginx:1.25 # Use local Docker daemon images to avoid pull limits $ deputy diff --source docker-daemon python:3.11 python:3.12
$ deputy diff Comparing dependencies: main -> HEAD Dependency Changes: ↑ github.com/google/osv-scalibr @ 0.3.2 -> 0.3.4 [direct] ↑ golang.org/x/crypto @ 0.44.0 -> 0.46.0 [direct] + cloud.google.com/go/compute/metadata @ 0.9.0 [indirect] ↓ osv.dev/bindings/go @ 0.0.0-20250905014459-96958296f6f2 → 0.0.0-20250808040635-c189436f8791 [direct] Summary: + 1 packages ↑ 2 packages ↓ 1 package ✓ No vulnerabilities found
deputy proxy: enforce at download time
deputy proxy runs at download time. It supports Go, npm, PyPI, RubyGems, and OCI,
and it uses the same policy bundles you run in scan and diff, so the
decision stays consistent. This is where your toolchain reaches out to the internet. It's a
great place to enforce guardrails before a dependency lands in a lockfile or image layer.
Policies live in the repo and enforce guardrails on every download while you work.
$ deputy proxy go --policy policy/ci/security-gate.yaml -- go get github.com/example/pkg@latestThe exact same policies run inside the pipeline during installs and image pulls.
$ deputy proxy npm --policy policy/ci/license-allowlist.yaml -- npm ci
Central proxy with JWT or OIDC auth and shared policy bundles for teams.
$ deputy proxy serve --config proxy.yamlPolicy decisions stay the same. Local and CI use repo policies. Remote adds auth and shared bundles.
If you're thinking about rolling it out beyond a single repo, the proxy rollout guide covers the operational rollout details. The proxy docs cover config and auth.
# Run commands through the proxy with policies $ deputy proxy go --policy policy/ci/security-gate.yaml -- go get github.com/example/pkg@latest $ deputy proxy npm --policy policy/ci/license-allowlist.yaml -- npm install # Run a standalone proxy service $ deputy proxy template > proxy.yaml $ deputy proxy serve --config proxy.yaml
Composable workflows
Most commands can emit JSON, so you can treat them like building blocks: pipe outputs around or
save them as artifacts. A scan can feed triage or a fix plan. An sbom can be generated once and scanned later. In CI, you can stash the JSON
output and SBOM alongside build artifacts, which keeps "what did we know then?" conversations short.
# Generate a JSON scan report $ deputy scan --format json --output scan.json # Turn that report into a remediation plan $ deputy fix --report scan.json # Or stream the report directly into fix $ deputy scan --format json | deputy fix --report -
# Generate an SPDX SBOM $ deputy sbom --format spdx-json --output sbom.spdx.json # Scan the SBOM later $ deputy scan sbom sbom.spdx.json
deputy fix: remediation plans
deputy fix prints an explicit plan first, because dependency changes deserve the
same review as code changes. You can review it, export it, or apply it. If you run an agent,
keep it behind a sandbox and keep review checkpoints in place. More in the fix docs.
# Generate a plan $ deputy fix # Save a plan for review or approval $ deputy fix --format json --output fix-plan.json # Apply fixes automatically $ deputy fix --apply . # Ask an agent to help implement upgrades $ deputy fix --agent claude --agent-full-auto
$ deputy fix --agent claude --agent-full-auto Remediation Plan: Target: /repo/deputy Commit: b0f72a33a9a74cf1d19c869971c342871f5630ab • Apply dependency upgrades (1 total, 2 runnable) go.mod: › go get github.com/google/osv-scalibr@0.3.4 ↻ go mod tidy Agent Remediation WARNING: Full-auto mode enabled: commands and file writes will execute without approval ## Summary Successfully executed the remediation plan: Completed actions: 1. ✓ Updated github.com/google/osv-scalibr from v0.3.3 to v0.3.4 2. ✓ Ran go mod tidy to clean up dependencies (added github.com/tink- crypto/tink-go/v2 v2.4.0 as a new transitive dependency) 3. ✓ All tests passed (113 test packages executed successfully) The dependency update has been applied and verified. The repository is now using the patched version of osv-scalibr. ✦ claude · 58.9s
Full-auto mode executes commands and writes files without approval. Treat it like a power tool
(it can be dangerous if misused). Use it in controlled environments, and pair it with builtin
review safeguards like --review, or normal branch protections, pre-merge checks,
etc.
deputy exec: safer installs and builds
If you don't fully trust npm install, you're not being paranoid. deputy exec runs package manager commands and build scripts inside a sandbox with
workspace isolation, file masking, network allowlists, and resource limits. The point isn't
magic safety. It's about shrinking the blast radius. Use --review to inspect
changes before syncing them back. Full flag details are in the exec docs.
# Isolate workspace and mask common secrets during npm install $ deputy exec --runtime docker \ --workspace-isolation snapshot \ --mask-preset supply-chain \ --image node:22-alpine \ -- npm install # Review changes before syncing them back $ deputy exec --workspace-isolation snapshot --review -- npm audit fix # VM-based isolation on macOS (VZ plugin) $ deputy exec --runtime plugin --plugin vz \ --workspace-isolation snapshot \ --review \ -- npm install
| Runtime | Platform | Notes |
|---|---|---|
docker | All | Container isolation (default). |
gvisor | Linux | Stronger isolation via gVisor. |
plugin (vz) | macOS ARM64 | VM-based isolation. |
sandbox-exec | macOS | Host sandboxing (more limited). |
none | All | No sandboxing (trusted only). |
| Mode | What it does |
|---|---|
direct | Mount workspace directly (fastest). |
snapshot | Copy workspace to a temp dir. |
overlay | Copy‑on‑write overlay (Linux). |
tmpfs | Memory‑backed workspace (ephemeral). |
git-worktree | Isolate via a Git worktree. |
Use --review to inspect and confirm workspace changes before they sync back to
your repo. For a manual audit, add --preserve-workspace to keep the isolated
workspace around. Filesystem mode (--mode) is separate from workspace isolation:
the default is workspace-write; for analysis‑only runs, use --mode read-only.
VM isolation helps prevent host-level compromise, but it doesn't make supply chain risk go away. A malicious package can still poison the build output or artifact you ship. Use defense-in-depth, keep blast radius small, and treat sandboxing as one (useful) layer, not a guarantee.
VZ is a sandbox runtime plugin that boots a lightweight Linux VM on macOS Apple Silicon using
the Apple Virtualization.framework (via Code‑Hex/vz). It's a good fit when you want
VM-level isolation without even needing Docker, and it's a concrete example of how
customizable sandbox runtime plugins work in Deputy. What's really cool: boot time is very
fast in the example plugin (≈1-2s), but it depends on the rootfs and what's installed inside
the VM.
Isolation mode decides how the workspace is staged before it is mounted.
Boot assets live under ~/.deputy/vz/* and attach via virtio‑blk.
/workspace/ (rootfs)/ and /workspace—nothing else from the
host.The hypervisor is the security boundary: the host treats everything inside the VM (kernel, userspace, your command) as untrusted.
Setup details: VZ plugin example.
Agents, but with guardrails
Agents are optional. If you pass --agent, Deputy can hand the context to an
assistant for explanations, triage, or help applying a plan. Different agents run in different
sandboxes (and may or may not have network access), so test the one you plan to use. Keep them
on a leash: use read-only for analysis, and pair workspace-write with
review checkpoints. The agents guide covers sandboxes and tradeoffs.
# Explain a vulnerability $ deputy explain --agent codex CVE-2021-44228 # Prioritize findings $ deputy triage --agent claude # Apply fixes with an agent in a safer sandbox $ deputy fix --agent claude --agent-sandbox workspace-write
Plugins: extend the inventory
If Deputy doesn't understand your ecosystem, you can teach it. Plugins are external executables (write them in Go or anything) that return packages to include in the inventory.
The plugins guide walks through packaging, discovery, and testing.
package main import ( "strings" "github.com/picatz/deputy/sdk/plugin" ) func main() { plugin.Main(&myExtractor{}) } type myExtractor struct{} func (e *myExtractor) Name() string { return "custom/myformat" } func (e *myExtractor) DisplayName() string { return "My Custom Format" } func (e *myExtractor) Ecosystem() string { return "custom" } func (e *myExtractor) Version() int { return 1 } func (e *myExtractor) Description() string { return "Extracts packages from .myformat files" } func (e *myExtractor) FilePatterns() []string { return []string{"*.myformat"} } func (e *myExtractor) FileRequired(path string, isDir bool, mode uint32, size int64) bool { return !isDir && strings.HasSuffix(path, ".myformat") } func (e *myExtractor) Extract(path string, contents []byte, root string) ([]*plugin.Package, error) { return []*plugin.Package{plugin.NewPackage("example-pkg", "1.0.0", "custom")}, nil }
Where Deputy Is Headed
I want dependency work to feel like a system, not a scavenger hunt: one inventory, one set of policies, and a straight line from "we have a problem" to "it's fixed". Predictable. Reviewable. Routine. Tractable. The goal is fewer fire drills and more reliable, explainable outcomes: every change is traceable, every finding is actionable, and the guardrails stay consistent no matter where the software lives. If Deputy does its job, the inventory becomes quiet infrastructure in the background, not another dashboard to babysit, just a source of truth that makes reviews faster and fixes repeatable. If you want to kick the tires, the repo is here. Bug reports, sharp-edge notes, and ideas are all welcome.