v_2-0_local-projects #1

Open
justin wants to merge 7 commits from v_2-0_local-projects into main
20 changed files with 489 additions and 214 deletions
Showing only changes of commit 4e7b2e0288 - Show all commits

View File

@ -28,13 +28,15 @@ safeclaude # launch Claude in this project's container
| Command | What it does |
| --- | --- |
| `safeclaude [PATH] [claude-args...]` | Launch Claude for a project (default: current dir). Anything extra is passed straight to `claude`. |
| `safeclaude build [PATH]` | Rebuild the project's container from scratch. |
| `safeclaude init [PATH]` | Create a starter `.safeclaude/` folder. |
| `safeclaude [claude-args...]` | Launch Claude for the project here. Anything extra is passed straight to `claude`. |
| `safeclaude build` | Rebuild the project's container from scratch. |
| `safeclaude init` | Create a starter `.safeclaude/` in the current directory. |
| `safeclaude envs` | List the containers and stored data safeclaude has created. |
| `safeclaude version` | Print the safeclaude version. |
A "project" is any folder (or a parent of it) that has a `.safeclaude/` folder,
so you can launch from a subdirectory and it'll still find the right one.
safeclaude always works on the project you're in — the current directory, or the
nearest parent that has a `.safeclaude/` folder, so launching from a
subdirectory still finds the right one.
## What goes in `.safeclaude/`

View File

@ -7,6 +7,7 @@ set -euo pipefail
SAFECLAUDE_VERSION="0.1.0"
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
SKELETON_DIR="$SCRIPT_DIR/skeleton"
BASE_IMAGE="safeclaude-base:latest"
die() { echo "safeclaude: $*" >&2; exit 1; }
@ -16,14 +17,15 @@ usage() {
safeclaude — run Claude in a locked-down container, one per project
Usage:
safeclaude [PATH] [claude-args...] Launch Claude for a project (default: current dir)
safeclaude build [PATH] Rebuild the project's container from scratch
safeclaude init [PATH] Create a starter .safeclaude/ folder
safeclaude envs List the containers and saved data safeclaude made
safeclaude version Print the safeclaude version
safeclaude help Show this help
safeclaude [claude-args...] Launch Claude for the project here (args pass to claude)
safeclaude build Rebuild the project's container from scratch
safeclaude init Create a starter .safeclaude/ in the current directory
safeclaude envs List the containers and saved data safeclaude made
safeclaude version Print the safeclaude version
safeclaude help Show this help
A project is any folder (or a parent of it) that has a .safeclaude/ folder.
Run from inside a project — the current directory, or the nearest parent that
has a .safeclaude/ folder.
EOF
}
@ -74,13 +76,9 @@ ensure_project_image() {
}
cmd_run() {
local target="${1:-.}"; [ $# -ge 1 ] && shift || true
# If the first argument isn't a folder, it was meant for claude — so hand it
# back to claude and use the current directory as the project.
if [ ! -d "$target" ]; then set -- "$target" "$@"; target="."; fi
local proj; proj="$(find_project_root "$target")" \
|| die "no .safeclaude/ found in '$target' or any parent — run: safeclaude init"
# Always operate on the current directory's project; every argument is for claude.
local proj; proj="$(find_project_root ".")" \
|| die "no .safeclaude/ found here or in any parent — run: safeclaude init"
ensure_base
local image; image="$(ensure_project_image "$proj")"
@ -116,8 +114,8 @@ cmd_run() {
}
cmd_build() {
local proj; proj="$(find_project_root "${1:-.}")" \
|| die "no .safeclaude/ found in '${1:-.}' or any parent — run: safeclaude init"
local proj; proj="$(find_project_root ".")" \
|| die "no .safeclaude/ found here or in any parent — run: safeclaude init"
ensure_base
ensure_project_image "$proj" 1 >/dev/null
echo "[safeclaude] env ready for $(proj_name "$proj")"
@ -133,97 +131,33 @@ cmd_envs() {
}
cmd_init() {
local target="${1:-.}"
[ -d "$target" ] || die "not a directory: $target"
local sc; sc="$(cd "$target" && pwd)/.safeclaude"
local sc; sc="$(pwd)/.safeclaude"
[ -e "$sc" ] && die ".safeclaude/ already exists at $sc"
mkdir -p "$sc/hooks" "$sc/cache"
[ -d "$SKELETON_DIR" ] || die "skeleton not found at $SKELETON_DIR"
# Record which safeclaude version created this, for future reference.
# Copy the template files (see skeleton/), then add the bits that are generated
# rather than templated: the cache dir and the version stamp.
mkdir -p "$sc"
cp -R "$SKELETON_DIR/." "$sc/"
mkdir -p "$sc/cache"
echo "$SAFECLAUDE_VERSION" > "$sc/version"
cat > "$sc/Dockerfile" <<'EOF'
# safeclaude builds this on top of its shared base. Everything here happens once
# and is cached, so it won't slow down your day-to-day launches.
FROM safeclaude-base:latest
# Add the system packages and language versions your project needs below.
# (You're root during the build, so apt just works.)
#
# Example — Ruby + Postgres client + headless Chrome (see the repo's example/):
#
# RUN apt-get update && apt-get install -y --no-install-recommends \
# build-essential libssl-dev libreadline-dev zlib1g-dev \
# libffi-dev libyaml-dev libpq-dev socat chromium chromium-driver
#
# Tip: pin specific language versions here (install one Ruby, one Node, etc.)
# rather than a version manager — a project only needs one. See the repo's
# example/ for how.
EOF
cat > "$sc/hooks/README.md" <<'EOF'
# Hooks
Each `*.sh` file here runs when the container starts, with your project available
at `/code`. They run in name order, so prefix them with numbers (`10-`, `20-`)
to control the sequence.
Because they run every launch, keep them quick and make them safe to re-run —
check whether the work is already done before doing it. If one fails, startup
stops rather than continuing half-configured.
Edits take effect on the next launch (no rebuild needed). Rename a
`*.sh.example` file to `*.sh` to turn it on.
EOF
cat > "$sc/hooks/10-setup.sh" <<'EOF'
#!/bin/bash
# Runs every time the container starts, with your project at /code. Put your
# project's startup setup here — installing dependencies, preparing services,
# and so on. It's empty by default; add what you need.
#
# Keep it safe to run every launch: check whether something is already done
# before doing it, so repeat launches stay quick.
#
# Need to keep things between runs? Write them to /code/.safeclaude/cache. That
# folder lives on the host (not inside the container), so it survives rebuilds
# and `docker volume` resets, and it's gitignored so it won't land in your repo.
# It's a good home for installed dependencies, downloads, or "already did this"
# markers.
#
# For example, you might install gems into the cache, run `npm install`, or wait
# for a service to come up. See the repo's example/ for a worked version.
set -euo pipefail
EOF
cat > "$sc/.env.example" <<'EOF'
# Put secrets here, then copy this file to .env (which stays out of git).
# Anything in .env is handed to the container when it starts — for example, a
# token for a private gem source:
# BUNDLE_GEMS__GRAPHQL__PRO=user:token
EOF
cat > "$sc/.gitignore" <<'EOF'
.env
cache/
EOF
echo "[safeclaude] created $sc"
echo " - edit Dockerfile to add the packages and language versions you need"
echo " - edit hooks/10-setup.sh for any startup setup"
echo " - then run: safeclaude $target"
echo " - then run: safeclaude"
}
main() {
local cmd="${1:-run}"
case "$cmd" in
build) shift; cmd_build "${1:-.}" ;;
init) shift; cmd_init "${1:-.}" ;;
build) cmd_build ;;
init) cmd_init ;;
envs|ls|list) cmd_envs ;;
version|-v|--version) echo "safeclaude $SAFECLAUDE_VERSION" ;;
help|-h|--help) usage ;;
run) shift; cmd_run "$@" ;;
*) cmd_run "$@" ;; # default: PATH and/or claude args
*) cmd_run "$@" ;; # default: all args go to claude
esac
}

4
skeleton/.env.example Normal file
View File

@ -0,0 +1,4 @@
# Put secrets here, then copy this file to .env (which stays out of git).
# Anything in .env is handed to the container when it starts — for example, a
# token for a private gem source:
# BUNDLE_GEMS__GRAPHQL__PRO=user:token

2
skeleton/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.env
cache/

16
skeleton/Dockerfile Normal file
View File

@ -0,0 +1,16 @@
# safeclaude builds this on top of its shared base. Everything here happens once
# and is cached, so it won't slow down your day-to-day launches.
FROM safeclaude-base:latest
# Add the system packages and language versions your project needs below.
# (You're root during the build, so apt just works.)
#
# Example — Ruby + Postgres client + headless Chrome (see the repo's example/):
#
# RUN apt-get update && apt-get install -y --no-install-recommends \
# build-essential libssl-dev libreadline-dev zlib1g-dev \
# libffi-dev libyaml-dev libpq-dev socat chromium chromium-driver
#
# Tip: pin specific language versions here (install one Ruby, one Node, etc.)
# rather than a version manager — a project only needs one. See the repo's
# example/ for how.

17
skeleton/hooks/10-setup.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/bash
# Runs every time the container starts, with your project at /code. Put your
# project's startup setup here — installing dependencies, preparing services,
# and so on. It's empty by default; add what you need.
#
# Keep it safe to run every launch: check whether something is already done
# before doing it, so repeat launches stay quick.
#
# Need to keep things between runs? Write them to /code/.safeclaude/cache. That
# folder lives on the host (not inside the container), so it survives rebuilds
# and `docker volume` resets, and it's gitignored so it won't land in your repo.
# It's a good home for installed dependencies, downloads, or "already did this"
# markers.
#
# For example, you might install gems into the cache, run `npm install`, or wait
# for a service to come up. See the repo's example/ for a worked version.
set -euo pipefail

12
skeleton/hooks/README.md Normal file
View File

@ -0,0 +1,12 @@
# Hooks
Each `*.sh` file here runs when the container starts, with your project available
at `/code`. They run in name order, so prefix them with numbers (`10-`, `20-`)
to control the sequence.
Because they run every launch, keep them quick and make them safe to re-run —
check whether the work is already done before doing it. If one fails, startup
stops rather than continuing half-configured.
Edits take effect on the next launch (no rebuild needed). Rename a
`*.sh.example` file to `*.sh` to turn it on.