removed path, moved init to skeleton folder

This commit is contained in:
Justin
2026-06-20 11:07:48 -04:00
parent ecfdbd9f98
commit 4e7b2e0288
7 changed files with 83 additions and 96 deletions

View File

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

View File

@ -7,6 +7,7 @@ set -euo pipefail
SAFECLAUDE_VERSION="0.1.0" SAFECLAUDE_VERSION="0.1.0"
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
SKELETON_DIR="$SCRIPT_DIR/skeleton"
BASE_IMAGE="safeclaude-base:latest" BASE_IMAGE="safeclaude-base:latest"
die() { echo "safeclaude: $*" >&2; exit 1; } die() { echo "safeclaude: $*" >&2; exit 1; }
@ -16,14 +17,15 @@ usage() {
safeclaude — run Claude in a locked-down container, one per project safeclaude — run Claude in a locked-down container, one per project
Usage: Usage:
safeclaude [PATH] [claude-args...] Launch Claude for a project (default: current dir) safeclaude [claude-args...] Launch Claude for the project here (args pass to claude)
safeclaude build [PATH] Rebuild the project's container from scratch safeclaude build Rebuild the project's container from scratch
safeclaude init [PATH] Create a starter .safeclaude/ folder safeclaude init Create a starter .safeclaude/ in the current directory
safeclaude envs List the containers and saved data safeclaude made safeclaude envs List the containers and saved data safeclaude made
safeclaude version Print the safeclaude version safeclaude version Print the safeclaude version
safeclaude help Show this help 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 EOF
} }
@ -74,13 +76,9 @@ ensure_project_image() {
} }
cmd_run() { cmd_run() {
local target="${1:-.}"; [ $# -ge 1 ] && shift || true # Always operate on the current directory's project; every argument is for claude.
# If the first argument isn't a folder, it was meant for claude — so hand it local proj; proj="$(find_project_root ".")" \
# back to claude and use the current directory as the project. || die "no .safeclaude/ found here or in any parent — run: safeclaude init"
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"
ensure_base ensure_base
local image; image="$(ensure_project_image "$proj")" local image; image="$(ensure_project_image "$proj")"
@ -116,8 +114,8 @@ cmd_run() {
} }
cmd_build() { cmd_build() {
local proj; proj="$(find_project_root "${1:-.}")" \ local proj; proj="$(find_project_root ".")" \
|| die "no .safeclaude/ found in '${1:-.}' or any parent — run: safeclaude init" || die "no .safeclaude/ found here or in any parent — run: safeclaude init"
ensure_base ensure_base
ensure_project_image "$proj" 1 >/dev/null ensure_project_image "$proj" 1 >/dev/null
echo "[safeclaude] env ready for $(proj_name "$proj")" echo "[safeclaude] env ready for $(proj_name "$proj")"
@ -133,97 +131,33 @@ cmd_envs() {
} }
cmd_init() { cmd_init() {
local target="${1:-.}" local sc; sc="$(pwd)/.safeclaude"
[ -d "$target" ] || die "not a directory: $target"
local sc; sc="$(cd "$target" && pwd)/.safeclaude"
[ -e "$sc" ] && die ".safeclaude/ already exists at $sc" [ -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" 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 "[safeclaude] created $sc"
echo " - edit Dockerfile to add the packages and language versions you need" echo " - edit Dockerfile to add the packages and language versions you need"
echo " - edit hooks/10-setup.sh for any startup setup" echo " - edit hooks/10-setup.sh for any startup setup"
echo " - then run: safeclaude $target" echo " - then run: safeclaude"
} }
main() { main() {
local cmd="${1:-run}" local cmd="${1:-run}"
case "$cmd" in case "$cmd" in
build) shift; cmd_build "${1:-.}" ;; build) cmd_build ;;
init) shift; cmd_init "${1:-.}" ;; init) cmd_init ;;
envs|ls|list) cmd_envs ;; envs|ls|list) cmd_envs ;;
version|-v|--version) echo "safeclaude $SAFECLAUDE_VERSION" ;; version|-v|--version) echo "safeclaude $SAFECLAUDE_VERSION" ;;
help|-h|--help) usage ;; help|-h|--help) usage ;;
run) shift; cmd_run "$@" ;; run) shift; cmd_run "$@" ;;
*) cmd_run "$@" ;; # default: PATH and/or claude args *) cmd_run "$@" ;; # default: all args go to claude
esac 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.