#!/usr/bin/env bash # tokenfund: your agent keeps working on 20vision/hide-and-clone until your limit. Stop any time with Ctrl-C. # The agent runs as a separate computer user with no root and no access to your files, inside Claude Code's own sandbox. # Your GitHub login stays with you: this script opens the pull requests, and the agent never sees that login. set -uo pipefail API=https://tokenfund.myclawn.com; SLUG='20vision-hide-and-clone'; REPO='20vision/hide-and-clone'; BUDGET=100000; MODEL='' say() { printf '\n\033[1;35mtokenfund\033[0m %s\n' "$*"; } die() { say "$*"; exit 1; } for c in git curl gh claude; do command -v "$c" >/dev/null || die "Please install $c first."; done gh auth status >/dev/null 2>&1 || die "Please run: gh auth login. Then press + again." WORK="$(cd "$(mktemp -d)" && pwd -P)" P="$WORK/project" OS="$(uname -s)" W=tokenfund as_w() { sudo -n -iu "$W" "$@"; } install_worker() { say "First run: creating a separate user called $W, with no root and no access to your files. This asks for your password once." sudo -v || die "Nothing was changed." if [ "$OS" = Linux ]; then for c in bwrap socat; do command -v "$c" >/dev/null || die "Please install bubblewrap and socat first, then press + again."; done id "$W" >/dev/null 2>&1 || sudo useradd --system --create-home --home-dir "/var/lib/$W" --shell /bin/bash "$W" || die "Could not create the user." sudo chmod 700 "/var/lib/$W" if [ -d /etc/apparmor.d ] && [ ! -f /etc/apparmor.d/bwrap ] && [ "$(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 2>/dev/null)" = 1 ]; then printf 'abi ,\ninclude \nprofile bwrap /usr/bin/bwrap flags=(unconfined) {\n userns,\n include if exists \n}\n' | sudo tee /etc/apparmor.d/bwrap >/dev/null sudo apparmor_parser -r /etc/apparmor.d/bwrap fi [ "$(stat -c %a "$HOME" | cut -c3)" = 0 ] || say "Note: other users can read your home folder. To close that, run: chmod o-rwx ~" elif [ "$OS" = Darwin ]; then id "$W" >/dev/null 2>&1 || sudo sysadminctl -addUser "$W" -home "/Users/$W" -password "$(openssl rand -hex 24)" >/dev/null 2>&1 || die "Could not create the user." sudo createhomedir -c -u "$W" >/dev/null 2>&1 else die "This system isn't supported yet." fi echo "$(id -un) ALL=($W) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/$W" >/dev/null; sudo chmod 440 "/etc/sudoers.d/$W" as_w bash -c 'curl -fsSL https://claude.ai/install.sh | bash' >/dev/null 2>&1 || die "Could not install Claude Code for the $W user." } as_w true 2>/dev/null && as_w test -x .local/bin/claude || install_worker if ! as_w .local/bin/claude auth status >/dev/null 2>&1; then say "Log in once for the $W user. It's the same Claude account, but your own files stay out of reach." sudo -iu "$W" .local/bin/claude auth login || die "Login didn't finish." fi SETTINGS='{"sandbox":{"enabled":true,"failIfUnavailable":true,"allowUnsandboxedCommands":false,"autoAllowBashIfSandboxed":true,"filesystem":{"denyRead":["~/"],"allowRead":["."]},"network":{"allowedDomains":["registry.npmjs.org","pypi.org","files.pythonhosted.org","crates.io","static.crates.io","index.crates.io","proxy.golang.org"]}}}' RUN="runs/$SLUG-$$" git clone --quiet --depth 50 "https://github.com/$REPO" "$P" || die "Could not download the project." cd "$P" || exit 1 # Connects your GitHub account with a temporary gist. Your GitHub login is never sent to us. TF_ID="$HOME/.config/tokenfund/key" if [ ! -s "$TF_ID" ]; then NONCE="$(curl -fsS -X POST "$API/api/id/start")" || die "The site can't be reached." GIST_URL="$(printf '%s\n' "$NONCE" | gh gist create -f tokenfund-proof.txt - 2>/dev/null)" || die "Please run: gh auth refresh -s gist. Then try again." GIST="${GIST_URL##*/}" sleep 2 KEY="$(curl -fsS -X POST "$API/api/id/verify?gist=$GIST&nonce=$NONCE&login=$(gh api user -q .login)")"; RC=$? gh api -X DELETE "gists/$GIST" >/dev/null 2>&1 [ "$RC" = 0 ] && [ -n "$KEY" ] || die "Could not verify your GitHub account. Try again in a minute." mkdir -p "$(dirname "$TF_ID")"; (umask 077; printf '%s' "$KEY" > "$TF_ID") say "GitHub account connected." fi TF_KEY="$(cat "$TF_ID")" S="" release() { [ -n "$S" ] && curl -fsS -m 10 -X POST "$API/api/release/$S" >/dev/null 2>&1; S=""; } trap 'release; as_w rm -rf "$RUN"; say "Stopped. Tokens used: $USED"; exit 0' INT TERM USED=0; PRS=0; EMPTY=0; FAILS=0 FORK="" if [ "$BUDGET" -gt 0 ]; then say "Working on $REPO up to $BUDGET tokens. Press Ctrl-C to stop."; else say "Working on $REPO with no limit. Press Ctrl-C to stop."; fi while :; do TURNS=40 if [ "$BUDGET" -gt 0 ]; then LEFT=$((BUDGET-USED)) [ "$LEFT" -lt 30000 ] && break TURNS=$((LEFT/8000)); [ "$TURNS" -lt 4 ] && TURNS=4; [ "$TURNS" -gt 40 ] && TURNS=40 fi git fetch --quiet --depth 50 origin HEAD && git reset --quiet --hard FETCH_HEAD && git clean -fdxq # The agent gets a fresh copy in its own account, so nothing it writes runs or lands here unchecked. as_w bash -c "rm -rf '$RUN' && mkdir -p runs && git clone --quiet --depth 1 'https://github.com/$REPO' '$RUN' && find '$RUN' -type l -not -path '*/.git/*' -delete" || die "Could not prepare the project." S="$(curl -fsS -X POST -H "x-tf-key: $TF_KEY" "$API/api/session/$SLUG?sha=$(git rev-parse HEAD)")" || die "This project is closed, your account is banned, or the site can't be reached." M=(); [ -n "$MODEL" ] && M=(--model "$MODEL") ARGS=(-p "Call the tokenfund start_session tool with project $SLUG and session $S, then follow what it returns." ${M[@]+"${M[@]}"} \ --setting-sources "" --settings "$SETTINGS" --strict-mcp-config --permission-mode dontAsk \ --mcp-config "{\"mcpServers\":{\"tokenfund\":{\"type\":\"http\",\"url\":\"$API/mcp\"}}}" \ --tools "Bash,Read,Edit,Write,Glob,Grep" \ --allowedTools "mcp__tokenfund" "Bash" "Read(./**)" "Edit(./**)" "Write(./**)" \ --max-turns "$TURNS" --output-format json) # Sent over stdin, because a login shell through sudo drops empty arguments. { printf 'cd %q && exec ~/.local/bin/claude' "$RUN"; printf ' %q' "${ARGS[@]}"; printf ' < /dev/null > ~/%q 2> ~/%q\n' "$RUN.json" "$RUN.err"; } | as_w bash -s as_w cat "$RUN.json" > "$WORK/run.json" 2>/dev/null as_w cat "$RUN.err" > "$WORK/run.err" 2>/dev/null T="$(curl -fsS -m 30 --data-binary @"$WORK/run.json" "$API/api/usage/$S" 2>/dev/null)" case "$T" in ''|*[!0-9]*) T=0 ;; esac USED=$((USED+T)) if [ "$T" = 0 ]; then release; FAILS=$((FAILS+1)) [ "$FAILS" -ge 2 ] && { sed -n 1,5p "$WORK/run.err" >&2; die "The agent couldn't run. Check your Claude Code login."; } continue fi FAILS=0 if ! as_w test -f "$RUN/.tokenfund-pr.md"; then release; EMPTY=$((EMPTY+1)); say "No finished change this round. Tokens used: $USED" [ "$EMPTY" -ge 2 ] && { curl -fsS -m 10 -X POST -H "x-tf-key: $TF_KEY" "$API/api/idle/$SLUG" >/dev/null 2>&1; say "Nothing more to do right now."; break; } continue fi EMPTY=0 as_w cat "$RUN/.tokenfund-pr.md" | head -c 20000 > "$WORK/pr.md" TITLE="$(head -n1 "$WORK/pr.md" | cut -c1-70)"; BODY="$(tail -n +2 "$WORK/pr.md")" as_w bash -c "cd '$RUN' && rm -f .tokenfund-pr.md && git add -A && git diff --cached --binary" > "$WORK/change.patch" if grep -qE '^(new file mode|old mode|new mode|deleted file mode) 120000' "$WORK/change.patch" || ! git apply --index "$WORK/change.patch" 2>/dev/null; then git reset --quiet --hard; release; say "Blocked a change that failed the safety check. It was not sent."; continue fi if git diff --cached --quiet; then release; continue; fi # Last check before anything leaves this computer. if git diff --cached --name-only | grep -qE '^(\.github/workflows/|\.claude/|\.mcp\.json|\.git)' \ || [ "$(git diff --cached --numstat | awk '{s+=$1+$2} END {print s+0}')" -gt 3000 ] \ || git diff --cached | grep -qE 'BEGIN [A-Z ]*PRIVATE KEY|sk-ant-|ghp_[A-Za-z0-9]{20}|github_pat_|AKIA[0-9A-Z]{16}|\.claude/\.credentials'; then release; say "Blocked a change that failed the safety check. It was not sent."; continue fi git -c user.useConfigOnly=false commit --quiet --no-verify -m "${TITLE:-tokenfund contribution}" if [ -z "$FORK" ]; then gh repo fork "$REPO" --remote --remote-name tokenfund-fork >/dev/null 2>&1 FORK=origin; git remote | grep -q '^tokenfund-fork$' && FORK=tokenfund-fork fi ME="$(gh api user -q .login)" if git push --quiet "$FORK" "HEAD:refs/heads/tokenfund-$S" 2>/dev/null \ && URL="$(gh pr create --repo "$REPO" --head "$ME:tokenfund-$S" --title "${TITLE:-tokenfund contribution}" --body "$BODY Contributed via tokenfund.myclawn.com · session $S")"; then curl -fsS -m 30 -X POST "$API/api/receipt/$S?pr=$URL" >/dev/null 2>&1 S=""; PRS=$((PRS+1)); say "Opened $URL · Tokens used: $USED" else release; say "Could not open the pull request. Trying the next task." fi done as_w rm -rf "$RUN" "$RUN.json" "$RUN.err" say "Done. $PRS pull requests, $USED tokens. Thank you!"