Skip to content
Open
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,16 @@ into the chosen root (project by default, `-g`/`--global` for user-wide,
`remove` to uninstall), and skips helper skills marked
`adapter_expose: false`. See `adapters/shared/README.md`.

### Dry-run sessions

To let skills act for real against a repository — push a branch, create an
issue — without anything being published, run them in a dry-run session.
`adapters/shared/dry-run-session.sh` clones the repository into a directory of
its own and starts the agent inside a process sandbox that writes only to the
clone and reaches only the agent's API; its `check` subcommand attempts every
write path from inside the sandbox and proves on the current machine that none
gets out. It needs the Anthropic Sandbox Runtime. See `adapters/shared/README.md`.

### Private journal

`clock-in` and `clock-out` work across two layers: the project the agent is
Expand Down
150 changes: 150 additions & 0 deletions adapters/shared/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,153 @@ Because the CLI installs every discovered skill, this also vendors the `grilling
helper (which the installer above filters out via `adapter_expose`) — so prefer
`npx skills add <source> --list` to preview, then pick exposed skills with
`--skill`.

## Dry-Run Session (`dry-run-session.sh`)

Cross-engine helper that runs an agent session on a clone of a repository which
**cannot publish anything**, so skills that push branches or create issues can
act for real — in a demonstration, a workshop, or a first look at an unfamiliar
project. The guarantee comes from a process sandbox around the session, not from
an instruction to the agent.

### Requirements

- The [Anthropic Sandbox Runtime](https://github.com/anthropics/sandbox-runtime):
`npm install -g @anthropic-ai/sandbox-runtime`, which provides `srt`. Without
it, `start`, `login` and `check` refuse to run.
- Node.js, which writes the sandbox policy (it is already there once `srt` is).
- On macOS, `ripgrep`. On Linux, `bubblewrap`, `socat` and `ripgrep`; see the
runtime's documentation for distribution-specific notes.

### Usage

```bash
./dry-run-session.sh setup <source> [target] # clone into a locked-down directory
./dry-run-session.sh login <target> # log Claude Code in, once per clone
./dry-run-session.sh check <target> # prove the boundary on this machine
./dry-run-session.sh start <target> # run Claude Code inside the session
./dry-run-session.sh start <target> -- <cmd ...> # ... or any other command
```

`<source>` is a local checkout or a clone URL; `<target>` defaults to
`<name>-dry-run` in the current directory. Set `ARCHITECTURE_KNOWLEDGE_TOOLKIT`
before `start` when the clone lives outside the toolkit's parent directory, so
the session inherits it.

| Variable | Default | Purpose |
|---|---|---|
| `DRY_RUN_ALLOWED_DOMAINS` | `api.anthropic.com *.anthropic.com claude.ai claude.com *.claude.com` | Domains the session may reach, separated by spaces. Set it for an agent other than Claude Code. |
| `DRY_RUN_SESSION_SRT` | `srt` | The sandbox runtime to use. |

### Behavior

- **A process sandbox around the whole session.** `start` runs the command
through `srt` with a policy written for this session, outside every writable
path. The session writes only to the clone and to the temporary directories of
`srt` and Claude Code (`/tmp/claude`, `/tmp/claude-<uid>` and `/tmp/claude-*`).
It cannot read `~/.ssh`, the `gh` and `glab` configuration, `~/.netrc`,
`~/.git-credentials`, `~/.claude` or `~/.claude.json`, and it reaches only the
allowed domains. GitHub and GitLab are denied explicitly, and a denial wins over
an allowance, so they stay unreachable even with `DRY_RUN_ALLOWED_DOMAINS="*"`.
- **A socket directory of its own.** Claude Code listens for messages from other
local agent sessions in `$XDG_RUNTIME_DIR/cc-socks`, by default `/tmp/cc-socks`
— next to the sockets of sessions that do not run in a dry run. The runtime's
`allowUnixSockets` opens binding *and* connecting for a whole path, so opening
that directory would let the dry run message a session outside it. `start`
instead points `XDG_RUNTIME_DIR` at a short directory created for the session,
opens Unix sockets only there, removes it when the session ends, and does not
pass on `CLAUDE_CODE_MESSAGING_SOCKET` or `CLAUDE_CODE_MESSAGING_TOKEN`.
- **Terminal control only for an interactive session.** Claude Code has to put
its terminal into raw mode, or the prompt shows escape sequences and takes no
input. `start` from a terminal therefore runs with the runtime's `allowPty`;
`login`, `check` and a `start` without a terminal on standard input do not.
- **Claude Code state stays with the clone.** The session runs with
`CLAUDE_CONFIG_DIR` set to `.git/dry-run-session/claude` inside the clone.
Settings, hooks, history and `.claude.json` written during a dry run live there
and are discarded with the clone; no session outside the dry run reads them.
Claude Code ties its login to that directory, so log in once per clone with
`login`. The login is the only run allowed to bind a local port, which the
OAuth callback needs; its token lands in that directory as
`.credentials.json`. `login` then marks Claude Code's onboarding as complete
there, because `claude auth login` does not: the first interactive `start`
would otherwise run the onboarding, ask to log in again, and fail on the port
it may not bind.
- **Only dry-run clones.** `start`, `login` and `check` refuse a directory that
`setup` did not prepare, recognized by its disabled push URL and its hook, so a
session never runs in an original checkout.
- **The session cannot loosen its guards.** The sandbox keeps the clone's hook
directory, its `.git/config` and `.claude/settings.local.json` unwritable.
- **A clone of its own.** The original checkout is never touched. The clone's
push URL is unusable, and a `pre-push` hook installed through a clone-local
`core.hooksPath` rejects every push, including a push to an explicitly named
URL.
- **A session without credentials.** `start` removes `GH_TOKEN`, `GITHUB_TOKEN`,
`GH_ENTERPRISE_TOKEN`, `GITLAB_TOKEN`, `GLAB_TOKEN` and the SSH agent socket,
points `gh` and `glab` at empty configuration, sets `GIT_SSH_COMMAND=false`,
resets every git credential helper, and disables terminal prompts.
- **Deny rules for Claude Code**, written to `.claude/settings.local.json` when
that file does not exist yet. An existing file is reported and left untouched,
never merged.
- **`check` tests the sandbox, not the layers around it.** From inside the
session it pushes to `origin` and to a local repository, with and without the
hook, writes outside the clone, changes the clone's git configuration and hook,
pushes over HTTPS with the credential helpers restored and over SSH with SSH
restored, to GitHub and to GitLab, calls `gh` with its own configuration, reads
`~/.ssh`, writes to and reads `~/.claude`, and connects to a stand-in for another
agent session's message socket. Every target either does not
exist or is thrown away, so even a failing boundary publishes nothing. Each
probe must fail for the right reason: one that fails only because a remote
answered without the target or the key is reported as `OPEN`. `check` also
reports whether Claude Code is logged in for the clone. Use a clone only when
`check` ends with `RESULT: tight`.

The clone guards, the credential removal and the deny rules are each something a
determined agent could undo from inside an unsandboxed session: `git push
--no-verify` skips the hook, `env -u` restores a credential helper, and the
original checkout is one `cd` away. They stay because they turn a mistake into a
clear message; the guarantee rests on the sandbox.

### What it does not do

- **It does not inspect what goes to an allowed domain.** The agent sends what
it reads to its own API, as in any session. That is not a publication to the
project, but anything added to `DRY_RUN_ALLOWED_DOMAINS` is reachable with
whatever the session can read.
- **The macOS login keychain stays readable**, because Claude Code keeps its
login there. A credential found in it cannot reach GitHub or GitLab, but it
could reach an allowed domain.
- **User-level Claude Code configuration does not apply.** Settings, skills,
agents and `CLAUDE.md` from `~/.claude` are not available inside a dry run; the
project's own configuration is.
- **The login token is a file in the clone.** A session can read it, although it
reaches only the allowed domains. End the login with
`start <target> -- claude auth logout` before deleting a clone, and do not copy
a clone that is logged in.
- **The login run can bind local ports**, and on macOS that also lets it reach
services on the loopback interface. It runs only `claude auth login`; log in
right after `setup`, before a session has written to the clone.
- **Claude Code's temporary directory `/tmp/claude-<uid>`** is shared with the
user's other Claude Code sessions and stays writable.
- **Other terminals are not isolated.** The runtime grants `allowPty` for every
`/dev/ttys*`, not only the session's own. An interactive session can open the
user's other terminals for writing, and any session can open them for reading
— with or without `allowPty` — and so compete for what is typed there.
Injecting input into another terminal with `TIOCSTI` is refused. While a dry
run is open, keep no other terminal with a shell or an agent session outside
the dry run.
- **Claude Code's version lock under `~/.local/state/claude` stays closed.**
Opening it, or `~/.local/share/claude/versions`, would let a session change the
Claude Code that later sessions outside the dry run execute. Claude Code logs
the failed lock as non-fatal and runs.
- **GitHub and GitLab are not readable either.** `gh`, `glab` and the public APIs
are unreachable from inside the session. Put the text of an issue into the
prompt, or into a file before `start`.
- **It is verified on macOS only**: with version 0.0.76 of the runtime and
Claude Code 2.1.270, including a complete login, and with version 1.0.0 and
Claude Code 2.1.273, including an interactive session. The policy is written for
Linux as well, but the runtime supports path globs only on macOS and blocks Unix
sockets there altogether, so a session cannot listen for messages from other
agent sessions; run `check` before relying on it. Windows is not supported.
- A `check` result holds for the machine it ran on.
- Local commits and file changes inside the clone are not blocked. That is the
point: the clone can act freely and be discarded afterwards.
Loading
Loading