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
11 changes: 11 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
}
]
}
],
"PreToolUse": [
{
"matcher": "Write|Edit|Bash|NotebookEdit|Agent",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/scripts/sandbox-write-guard.sh"
}
]
}
]
}
}
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ web_modules/
.local-developer
.claude/settings.local.json

# Staff sandbox — Roboterry's scratch workspace for staff-generated reports,
# exports, scripts, and CSVs. Contents must never be committed to git.
# Only the explanatory README is tracked so the directory always exists.
/sandbox/*
!/sandbox/README.md

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
Expand Down
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Every session runs in one of two modes. A SessionStart hook tells you which mode
- **Staff mode** (default) — the person is non-technical staff using the tool.
- **Developer mode** — the person is an engineer maintaining this repository. Active when a `.local-developer` file is present in the repo root (developers create it with `npm run local-developer`).

Tool use is never restricted by the harness. The mode does not change what you _can_ do — it changes how you _should_ behave. Follow the matching instruction set below.
The mode primarily changes how you _should_ behave, and you must follow the matching instruction set below. In staff mode it also changes what you _can_ do: a `PreToolUse` hook (`scripts/sandbox-write-guard.sh`) mechanically enforces the boundary — it allows `Write`/`Edit`/`NotebookEdit` only inside `sandbox/` and blocks `Bash` and `Agent` entirely. Developer mode is unrestricted. Treat the instructions as the contract and the hook as a backstop; do not try to work around it.

## Staff mode

Expand All @@ -21,9 +21,9 @@ Users are non-technical staff — not engineers.

Rules for staff mode:

1. **Do not create, modify, or delete any files**, even though the tools are available to you. Do not write reports, exports, or any other files to disk.
2. **Do not suggest creating a pull request**, and do not run or suggest shell commands. Users should never need a terminal.
3. **Treat all data as sensitive production data.** Do not save, export, or share results outside of this conversation.
1. **Only write files inside the `sandbox/` directory.** This git-ignored folder is your workspace for staff-requested reports, exports, scripts, CSVs, and other generated files. When a user asks you to generate or save something, write it there and tell them where to find it. Do not create, modify, or delete files anywhere else in the project, and never write outside the repository. (The sandbox-write-guard hook enforces this; writes outside `sandbox/` are blocked.)
2. **Do not suggest creating a pull request**, and do not run or suggest shell commands. Users should never need a terminal. (You may use file tools to write within `sandbox/` yourself; never ask the user to do it.)
3. **Treat all data as sensitive production data.** Files you write to `sandbox/` stay on the user's machine and are never committed to git, but they may contain sensitive data — remind the user to follow data-handling policies before sharing them. Do not transmit or share results outside this conversation and machine (e.g., email, uploads, external services).
4. **Answer questions using the available data tools** and present the results clearly. (The database connection is provided by a separate postgres MCP server that is configured outside of this branch; if no data tools are available, explain that the data connection is being set up and you cannot query yet.)
5. **Any data connection is read-only.** Only read/SELECT-style access is permitted. Never attempt to write, update, or delete data.

Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,21 @@ Claude translates your question into a database query, runs it, and presents the
- **Ask in plain English.** You don't need to know SQL or database terminology.
- **Be specific about time ranges.** "Last month" is better than "recently."
- **Ask follow-up questions.** You can refine your question based on previous results.
- **Results stay in the conversation.** Nothing is saved to files or shared anywhere — treat the conversation as ephemeral.
- **Results stay in the conversation by default.** Nothing is shared anywhere outside it.
- **Ask for a saved copy when you need one.** If you want a report, spreadsheet, or other file, just ask. Roboterry saves it to the `sandbox/` folder in the project — a private workspace on your computer that is never committed or shared. See `sandbox/README.md` for details.

## Security Notes

- The data connection is **read-only**. No data can be modified through this tool.
- Results may contain **sensitive production data**. Do not copy results into emails, documents, or other tools without following your organization's data handling policies.
- Sessions are **ephemeral** — results are not persisted after the conversation ends.
- Conversations are **ephemeral** — results are not persisted after the conversation ends unless you ask Roboterry to save them.
- The only place files are written is the git-ignored `sandbox/` directory. Its contents stay on your machine and can never be committed to git or pushed to GitHub. This is enforced mechanically by a `PreToolUse` hook (`scripts/sandbox-write-guard.sh`), not just by instructions — in staff mode, writes outside `sandbox/` and all shell commands are blocked.

## Developer Mode

Roboterry never restricts which tools are available. Instead, a SessionStart hook detects whether you are a developer and switches which instructions Claude follows (see `CLAUDE.md`):
A SessionStart hook detects whether you are a developer and switches which instructions Claude follows (see `CLAUDE.md`). In staff mode a `PreToolUse` hook also enforces the boundary mechanically; developer mode is unrestricted:

- **Staff mode** (default) — Claude behaves as a read-only, conversational data assistant.
- **Staff mode** (default) — Claude behaves as a read-only, conversational data assistant. Writes are confined to `sandbox/`, and shell commands and subagents are blocked.
- **Developer mode** — Claude behaves as a normal engineering session and may modify files, run commands, and open pull requests.

To enable developer mode, run:
Expand Down
17 changes: 17 additions & 0 deletions sandbox/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Sandbox

This is Roboterry's safe writing workspace. When you ask Roboterry to generate
something — a report, a spreadsheet/CSV, a script, a summary file — it is saved
here.

A few things to know:

- **Everything in this folder stays on your computer.** The entire `sandbox/`
directory is git-ignored (except this README), so nothing written here can
ever be committed or pushed to GitHub.
- **It is the only place Roboterry will write.** Roboterry will not create or
change files anywhere else in the project.
- **Treat its contents as sensitive.** Files here may contain production data.
Follow your organization's data-handling policies before sharing them.
- **Clear it out whenever you like.** You can safely delete anything in this
folder (except this README) — it is just scratch space.
2 changes: 1 addition & 1 deletion scripts/developer-mode-status.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
if [ -f "$REPO_ROOT/.local-developer" ]; then
echo "Roboterry developer mode is ACTIVE for this session. Follow the \"Developer mode\" instructions in CLAUDE.md: this is a normal engineering session and you may create, modify, and delete files, run shell commands, and propose pull requests."
else
echo "Roboterry developer mode is INACTIVE (staff mode) for this session. Follow the \"Staff mode\" instructions in CLAUDE.md: do not modify the repository, do not run or suggest shell commands, present results conversationally, and treat all data as sensitive."
echo "Roboterry developer mode is INACTIVE (staff mode) for this session. Follow the \"Staff mode\" instructions in CLAUDE.md: you may write generated files (reports, exports, CSVs, scripts) only inside the git-ignored sandbox/ directory, do not modify anything else in the repository, do not run or suggest shell commands, present results conversationally, and treat all data as sensitive."
fi
66 changes: 66 additions & 0 deletions scripts/sandbox-write-guard.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail

# ------------------------------------------------------------------
# Sandbox Write Guard (PreToolUse hook)
#
# Mechanically enforces the staff-mode boundary described in CLAUDE.md.
# Developer mode (the .local-developer flag exists) is unrestricted.
# In staff mode this hook:
# - allows Write/Edit/NotebookEdit only inside the sandbox/ directory
# - denies Bash (staff sessions never run shell commands)
# - denies Agent (subagents could write outside the guarded path)
#
# A denied tool call exits 2; Claude Code feeds the stderr message back
# to the model as the denial reason. This is a backstop for the CLAUDE.md
# instructions, not a replacement for them.
# ------------------------------------------------------------------

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"

# Developer mode: no restrictions.
if [ -f "$REPO_ROOT/.local-developer" ]; then
exit 0
fi

deny() {
printf '%s\n' "$1" >&2
exit 2
}

PAYLOAD="$(cat)"
TOOL="$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // empty')"

case "$TOOL" in
Bash)
deny "Staff mode: shell commands are disabled. Ask Terry to do this another way, or have a developer run it."
;;
Agent)
deny "Staff mode: subagents are disabled because they can write outside the sandbox/ workspace."
;;
Write | Edit | NotebookEdit)
TARGET="$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.file_path // .tool_input.notebook_path // empty')"
[ -n "$TARGET" ] || deny "Staff mode: could not determine the target path, so the write was blocked."

INSIDE="$(REPO_ROOT="$REPO_ROOT" TARGET="$TARGET" node -e '
const fs = require("fs");
const path = require("path");
const sandbox = fs.realpathSync(path.join(process.env.REPO_ROOT, "sandbox"));
const target = path.resolve(process.env.REPO_ROOT, process.env.TARGET);
// Resolve symlinks on the longest existing ancestor of the target,
// so a new file under a real sandbox path still resolves correctly.
let ancestor = target;
while (!fs.existsSync(ancestor) && ancestor !== path.dirname(ancestor)) {
ancestor = path.dirname(ancestor);
}
const resolved = path.resolve(fs.realpathSync(ancestor), path.relative(ancestor, target));
const rel = path.relative(sandbox, resolved);
const inside = rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
process.stdout.write(inside ? "yes" : "no");
')"

[ "$INSIDE" = "yes" ] || deny "Staff mode: files can only be written inside the sandbox/ directory. Blocked path: $TARGET"
;;
esac

exit 0