diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 0000000..fa797ef --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Make sure rtk is on PATH, so the PreToolUse hook in .claude/settings.json has +# something to call. Without this the sandbox starts every session with an empty +# container and the rewrite hook quietly no-ops. +# +# Deliberately never fails: a missing rtk costs tokens, not correctness, and a +# session that refuses to start because a compression tool did not download +# would be a much worse trade. + +set -uo pipefail + +RTK_VERSION="v0.45.0" +INSTALLER="https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh" + +export PATH="$HOME/.local/bin:$PATH" + +if command -v rtk >/dev/null 2>&1; then + exit 0 +fi + +# Only install unattended in the ephemeral web sandbox. On a personal machine, +# say what to run instead — dropping a binary into someone's ~/.local/bin +# without asking is not ours to do. +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + echo "rtk is not installed, so bash output will not be compressed." + echo "To install it: curl -fsSL $INSTALLER | sh" + exit 0 +fi + +# The version has to be pinned. The installer's default is to look up the latest +# tag through api.github.com, which the sandbox proxy answers with a 403 for +# every repository except this one, so the lookup fails and takes the install +# with it. Bump this by hand. +if ! curl -fsSL "$INSTALLER" 2>/dev/null | RTK_VERSION="$RTK_VERSION" sh >/dev/null 2>&1; then + echo "rtk $RTK_VERSION did not install; continuing without output compression." + exit 0 +fi + +echo "rtk $(rtk --version 2>/dev/null | awk '{print $2}') ready; bash output is compressed." diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..afcdbb8 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,25 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "command -v rtk >/dev/null 2>&1 && exec rtk hook claude || exit 0" + } + ] + } + ] + } +} diff --git a/.rtk/filters.toml b/.rtk/filters.toml new file mode 100644 index 0000000..d9bd43f --- /dev/null +++ b/.rtk/filters.toml @@ -0,0 +1,13 @@ +# Project-local RTK filters — commit this file with your repo. +# Filters here override user-global and built-in filters. +# Docs: https://github.com/rtk-ai/rtk#custom-filters +schema_version = 1 + +# Example: suppress build noise from a custom tool +# [filters.my-tool] +# description = "Compact my-tool output" +# match_command = "^my-tool\\s+build" +# strip_ansi = true +# strip_lines_matching = ["^\\s*$", "^Downloading", "^Installing"] +# max_lines = 30 +# on_empty = "my-tool: ok" diff --git a/CLAUDE.md b/CLAUDE.md index 98dcdf1..858e8b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,3 +67,48 @@ The suppression is a property of the sandbox's credential, so it cannot be fixed from inside a session. Giving the environment a personal access token to push with would make pushes and PRs behave normally for everyone. Until then, the dispatch step above is the workaround. + +## Bash output is filtered before it reaches the model + +`.claude/settings.json` registers a `PreToolUse` hook on `Bash` that rewrites +each command through [rtk](https://github.com/rtk-ai/rtk): `git status` runs as +`rtk git status`, and rtk compresses the output. Write commands normally — the +rewrite is transparent and costs nothing to invoke. + +Measured on this repo rather than quoted from rtk's README: + +| Command | Raw | Filtered | Saved | +| --- | --- | --- | --- | +| `git diff HEAD~3` | 521 KB | 28 KB | 95% | +| `grep -rn "fun " app/src/main` | 117 KB | 19 KB | 84% | +| `find . -name "*.kt"` | 7.7 KB | 1.7 KB | 79% | +| `git log -20` | 20 KB | 5.3 KB | 74% | +| `git diff --stat` | 4.0 KB | 4.0 KB | 1% | +| `ls` of a single package | 50 B | 62 B | −24% | + +The saving is all in the large outputs, which is the point — a full diff of a +few commits is otherwise most of a context window. Already-compact output grows +slightly, because rtk adds a header; at those sizes it does not matter. `rtk +gain` reports what the current session has actually saved. + +Two limits are worth knowing. The `Read`, `Grep` and `Glob` tools are not +`Bash`, so they never reach the hook and are unaffected; a deliberate `rg` or +`find` in a shell *is* filtered. And filtering never truncates a file: `cat` +becomes `rtk read`, which was checked byte-for-byte against this repo's longest +Kotlin source (`GameSetupScreen.kt`, 946 lines) and came back identical. Commit +messages survive the rewrite intact, quotes and colons included. To bypass +filtering for one command, run `rtk proxy `. + +## rtk is reinstalled at the start of every session + +The container starts empty, so `.claude/hooks/session-start.sh` installs rtk +before the session does anything. It pins `RTK_VERSION`, because the installer +otherwise asks `api.github.com` for the latest tag and the proxy answers 403 for +every repository except this one — the lookup fails and takes the install with +it. Bump that variable by hand. + +The hook is written not to matter when it fails. Outside the sandbox it installs +nothing and just prints the command to run, since dropping a binary into +someone's `~/.local/bin` uninvited is not its business; if the download fails it +says so and exits clean. With rtk absent the rewrite hook no-ops, and the only +consequence is verbose output. diff --git a/gradle.properties b/gradle.properties index b8b27b0..cf29b77 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,10 +16,10 @@ android.nonTransitiveRClass=true # ., a production tag is v. and a dev tag adds # -dev. It lives here rather than in the build file because the workflows have # to name the tag as well, and they read it straight out of this file — three -# copies of "1.1" would be three chances for a release to be tagged one thing +# copies of "1.2" would be three chances for a release to be tagged one thing # and built as another. # # Only the series is set by hand. The number after it is the CI run number, so # it keeps climbing on its own and an APK's versionCode never goes backwards # when this changes. -plgVersionSeries=1.1 +plgVersionSeries=1.2