Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,8 @@ stashbase scan install --all
stashbase scan uninstall pre-commit --file .husky/pre-commit
```

In a `stashbase agent run` session, these hooks need `allow_hooks = ["secret_scan"]` in the agent profile: the scan then runs on the host with your key, confined to the run's files. See [Agent Profiles](docs/agent-profiles.md#api-hooks).

### Dependency Security Hooks

Block suspicious package installs before they run. Install hooks for Codex, Claude Code, or Cursor:
Expand Down Expand Up @@ -546,8 +548,15 @@ stashbase agent hooks deps uninstall cursor
stashbase agent hooks deps uninstall codex --global
stashbase agent hooks deps uninstall claude --global
stashbase agent hooks deps uninstall cursor --global

# Install, check or remove in the Docker sandbox's home (shared by every Docker agent run)
stashbase agent hooks deps install claude --docker
stashbase agent hooks deps check claude --docker
stashbase agent hooks deps uninstall claude --docker
```

In a `stashbase agent run` session, the hook needs `allow_hooks = ["dependency_check"]` in the agent profile. A Docker run can't see your global hooks: use per-repository hooks or `--docker`. See [Agent Profiles](docs/agent-profiles.md#api-hooks).

Use `dependencies` as an alias for `deps`. The hook supports npm, Bun, pnpm, and Yarn. For package-specific installs, it sends only package names and versions to Stashbase. For project-wide installs like `npm ci`, it scans direct dependencies from `package.json` and uses versions from lockfiles when available. This is not a full dependency-tree audit; transitive dependencies are not scanned.

### Diagnose CLI Setup
Expand Down
2 changes: 2 additions & 0 deletions docs/agent-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ allow_hooks = ["dependency_check", "secret_scan"]

The child receives only a scoped local broker token, never your API key. Hooks are disabled by default.

`dependency_check` works with the hooks from `stashbase agent hooks deps install`. With the native backend they run your installed CLI, which asks the Agent Proxy to call the API. The Docker image has no Stashbase CLI, so in a Docker run a small stand-in is mounted as `stashbase` inside the container: it forwards the hook's input to the Agent Proxy, which runs `stashbase agent hooks` on the host, confined the same way as the secret scan below, and returns its answer. If the proxy can't be reached, the stand-in blocks the command instead of letting the install through unchecked. In a Docker run the agent can't see the global hook config in your home directory. Either install the hook per repository (without `--global`; the container sees the repo), or install it into the Docker sandbox's own home, which every Docker run shares: `stashbase agent hooks deps install claude --docker` (likewise `check` and `uninstall`).

`secret_scan` lets the git hooks from `stashbase scan install` run inside the sandbox. The hook sends only a token to the Agent Proxy, which runs `stashbase scan staged` (or `unpushed`) on the host, in the run's working directory, with your key. Because the agent can edit the repo's scan config, these scans ignore its `match` and `output-dir` settings; `excluded-files` and `ignored-secrets` still apply. The host-side scan is confined, so a symlink or a `.git` redirect planted by the agent can't point it at your other files. On Linux it sees only system files plus the run's working tree, git directories and the CLI binary. On macOS it can't read file contents anywhere users' data lives (home directories, `/Volumes`, `/tmp`, `/var/folders`, `/opt`) except those same run paths; system files stay readable, and file names and sizes stay visible because path lookup needs them. The profile's `deny_read` entries stay denied on both. This uses Seatbelt on macOS and bubblewrap on Linux; where neither is available (including Windows), a profile with `secret_scan` refuses to start. Restricted scans also stop at 1 MiB per changed file, 32 MiB in total, 10,000 files or 1,000 commits; larger changes have to be committed from outside the sandbox. The hook needs `curl` in the sandbox (the default Docker image has it). Hooks installed before this existed need `stashbase scan install` once more to pick up the sandbox-aware block. Without `secret_scan`, a scan hook inside the sandbox fails with a message pointing here.

## Audit Logs and Session Revocation
Expand Down
4 changes: 4 additions & 0 deletions docs/sandboxing.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ stashbase agent run --profile coding --docker-image node:22-alpine -- claude

Your global `git config user.name` and `user.email` (if configured on the host) are forwarded into the container as `GIT_AUTHOR_NAME`, `GIT_AUTHOR_EMAIL`, `GIT_COMMITTER_NAME`, and `GIT_COMMITTER_EMAIL`. This is the one piece of host configuration deliberately forwarded despite the filesystem allow-list, since it's authorship metadata, not a credential — without it, `git commit` inside the sandbox fails with no identity configured. It does not grant push access: `git push` (or any other authenticated git operation) still needs a real credential, wired through `[secrets]` like `GITHUB_TOKEN`, or run from outside the sandbox. Raw SSH keys are never forwarded. A profile that explicitly sets one of these four env vars itself takes precedence over the forwarded host value.

### Dependency hooks

The dependency check hooks (`allow_hooks = ["dependency_check"]`) work in the Docker sandbox without the Stashbase CLI in the image: a stand-in `stashbase` is mounted at `/usr/local/bin/stashbase`, and `stashbase agent hooks` runs on the host instead, confined like the secret scan below. The agent in the container sees project-level hook configs and its own home's; `stashbase agent hooks deps install <agent> --docker` installs into the latter for every Docker run. See [Agent Profiles](agent-profiles.md#api-hooks).

### Secret scan hooks

Git hooks installed with `stashbase scan install` work in the sandbox when the profile sets `allow_hooks = ["secret_scan"]`. The sandbox has neither the Stashbase CLI nor your API key, so the hook asks the Agent Proxy to run the scan on the host against the same working directory, with `curl` and a per-run token. Findings come back to the agent, and the commit or push is blocked, exactly as outside the sandbox. The scan itself runs confined (Seatbelt on macOS, bubblewrap on Linux): outside system files, it can read only the run's working tree, its git directories and the CLI binary, never your other files that the agent points it at through symlinks or `.git` redirects; `secret_scan` is unavailable on Windows for that reason. Like any git hook, it is skipped by `git commit --no-verify` — a safety net, not an enforcement boundary. See [Agent Profiles](agent-profiles.md#api-hooks).
Expand Down
5 changes: 5 additions & 0 deletions src/cmd/deps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ pub struct AgentHookTarget {
/// Apply to the global configuration instead of only this repository
#[arg(long)]
pub global: bool,

/// Apply to the Docker sandbox's home, which every Docker-backend agent
/// run shares, instead of this machine's configuration
#[arg(long, conflicts_with = "global")]
pub docker: bool,
}

#[derive(Clone, Copy, Debug, clap::ValueEnum)]
Expand Down
5 changes: 4 additions & 1 deletion src/handlers/agent/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,10 @@ fn validate_hook_capabilities(profile: &AgentProfile) -> Vec<Check> {
})
.collect();
}
if profile.allow_hooks.iter().any(|hook| hook == "secret_scan") {
let docker = profile.sandbox.backend == crate::models::agent::SandboxBackend::Docker;
let runs_confined =
|hook: &String| hook == "secret_scan" || (docker && hook == "dependency_check");
if profile.allow_hooks.iter().any(runs_confined) {
if let Some(reason) = crate::handlers::run::scan_sandbox::unavailable_reason() {
return vec![fail("Hook capability", format!("{reason}."))];
}
Expand Down
Loading
Loading