Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
f9a8094
Add feature-spec skill for pre-implementation documentation
zhumo Jan 8, 2026
394ef60
Reframe interview as engineering requirements, not product discovery
zhumo Jan 8, 2026
d8ef995
let it skip tests if no tests exist
zhumo Jan 8, 2026
8aecf48
more logging for sending usage data
zhumo Jan 8, 2026
826564e
remove unused ralph wiggum plugin
zhumo Jan 8, 2026
bedc45d
remove hookify plugin
zhumo Jan 8, 2026
a4d1c91
command for addressing pr comments
zhumo Jan 11, 2026
6afce88
tighten up failing PostToolUse hook
zhumo Jan 11, 2026
4869e80
show simplecov data after running specs
zhumo Jan 11, 2026
3789acc
add instruction to avoid duplication
zhumo Jan 12, 2026
7eeccd0
tell it to stop guarding!
zhumo Jan 12, 2026
c0b1ffc
allow claude to run rspec
zhumo Jan 12, 2026
f463354
try to be more specific about not holding up the program
zhumo Jan 12, 2026
1edde38
run tests before stopping
zhumo Jan 12, 2026
75d114f
update symlink script to not create nested symlinks inside claude/com…
zhumo Jan 12, 2026
27af10e
getting all comments on a PR using gh command
zhumo Jan 15, 2026
ebbcaab
simplify git and github cli perissions for claude
zhumo Jan 23, 2026
3c1cab1
allow webfetch
zhumo Jan 23, 2026
1a3ae83
deny claude to look at the rails credentias
zhumo Jan 23, 2026
1cee353
remove hook that seems to be blocking usage data being sent up
zhumo Jan 23, 2026
32ea7f0
set up sending to dev environment
zhumo Jan 23, 2026
e4f0fe5
adapt to new jsonl structure for token usage uploading
zhumo Jan 23, 2026
696e3d6
prevent git push master or main by claude
zhumo Jan 23, 2026
5f090e6
Fix hook exit codes to show output to user (#9)
zhumo Jan 23, 2026
e243e5d
Update new-worktree command to give explicit handoff instructions
zhumo Jan 23, 2026
ba1c0db
Add catch-up sync for usage data (#10)
zhumo Jan 24, 2026
ba3de14
Fix usage data collection for subagents (#11)
zhumo Jan 24, 2026
0fbe181
fix the off by one error
zhumo Jan 24, 2026
b83e781
allow running rubocop
zhumo Jan 27, 2026
682e315
dont let claude write to the gemfile willy nilly
zhumo Jan 27, 2026
e5a3b48
prevnt destructive rails db changes
zhumo Jan 27, 2026
0e87e20
shortcut for claude
zhumo Jan 29, 2026
ce71412
git pull uses rebase instead of merge
zhumo Jan 29, 2026
84fa90f
alias for git fetch -a and git push, and some reformatting of the file
zhumo Jan 29, 2026
deb751b
remove the env thing because that was for claude code proxy
zhumo Jan 29, 2026
d31f927
prepend [CLAUDE] to address pr comments skill
zhumo Jan 29, 2026
7137af4
improve worktree creation
zhumo Jan 31, 2026
77d23eb
shortcut for git pull
zhumo Jan 31, 2026
f2e99cf
shortcut for fetch and rebase onto master
zhumo Jan 31, 2026
c3d6e97
ask it to use the chrome browser plugin after each round of changes
zhumo Jan 31, 2026
593fc30
update vimrc to include testing hook
zhumo Jan 31, 2026
6d1c456
remove vimrc settings that are already defaults in neovim
zhumo Feb 1, 2026
15eab73
remove t_Co=256, neovim handles terminal colors automatically
zhumo Feb 1, 2026
8bd62f7
set e2e/ as folder for playwright
zhumo Feb 1, 2026
53a8bf2
more complexity on address-pr-comments
zhumo Feb 5, 2026
502a01b
enable webfetch everywhere
zhumo Feb 6, 2026
fa9e1f0
command to refresh branch
zhumo Feb 6, 2026
b882636
ask permission before git forcing
zhumo Feb 9, 2026
16aec46
allow running npm commands
zhumo Feb 11, 2026
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
1 change: 0 additions & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"permissions": {
"allow": [
"WebFetch(domain:gist.github.com)"
]
}
}
6 changes: 6 additions & 0 deletions claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@
- No argument validation at function start
- No try-catch unless explicitly asked
- Make minimal changes when fixing issues - don't refactor unrelated code
- Don't guard against missing attributes unless explicitly asked. Let the system fail.

# Git Workflow

- `gh` is aliased to `git hist`, so use `command gh` for GitHub CLI
- Prefer rebase over merge when updating feature branches from master/main
- Name worktrees as `{repo-name}--{branch-name}`
- When fetching comments with the `gh` utility, use --paginate to make sure you get all of the comments.

# Process

- Update CLAUDE.md after significant code changes
- Write out purpose before running bash scripts
- Reflect on tool results before proceeding
- After finishing each round of changes, go click through the application using the Chrome browser plugin. Verify the functionality and any functionality that was impacted by the last round of changes.

# Running Bash Commands
- When specifying a path, use paths relative to the current working directory, rather than absolute paths.
- There is no need to cd into the local directory. Instead, first check your current working directory. If it is the repo's directory, then run the command.
- When running commands, unless there is a specific reason to combine stderr and stdout using `2>&1`, don't do it.

# Maintaining CLAUDE.md
- There should be one place where information is maintained. Rely on the underlying framework to do this. Don't create a separate list. (For example, in Rails, don't list the routes in CLAUDE.md. The routes are already documented in config/rails.rb.) Instead, in the Claude.md file, point to the file where the information is stored.
13 changes: 13 additions & 0 deletions claude/commands/address-pr-comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
User input: $ARGUMENTS

Take the comments from the user input (it may be blank) and user comments on the corresponding PR to this branch (if it exists).

The user probably put in two kinds of comments:
1. Questions for discussion or clarification
2. Direct commands to change how code words

For type #1 comments, respond #1 directly on the relevant comment. Prepend [CLAUDE] to make it clear it's coming from you.

For type #2 comments, make new branch called updates/pr-123-202601011345 (updates/pr-[pr number]-[timestamp yyyymmddhhmm]) changes in code then create a new PR wherein the new branch is being merged into the underlying feature branch. On the relevant parts of the PR, put a comment that has the full comment history which drove the change. [Mo Zhu] for my comments [Claude] for your comments.


19 changes: 14 additions & 5 deletions claude/commands/new-worktree.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
$ARGUMENTS

Create a new worktree for this feature. Worktree name should be {repo-name}--{worktree-name}. Put the worktree in a folder that a sibling of the current repo.

After creating the new worktree:
1. use the /add-dir command to add the new worktree into current Claude's permission instance.
2. `cd` into the new worktree as your new working directory
Create a new worktree for this feature. Worktree name should be {repo-name}--{worktree-name}. Put the worktree in a folder that is a sibling of the current repo.

Be sure to specify in the git command to create a worktree off of master or main branch, not the current branch.

After creating the worktree:
1. Run any installation script necessary such as npm install or bundle install
2. Copy any .env or secret credentials files from the master repo to the worktree repo

Once all of the above is done, output:

```
Worktree created. Open a new terminal tab and run:
cd {worktree-path} && claude
```

Where {worktree-path} is the relative path to the worktree (e.g., `../myrepo--feature-branch`).
3 changes: 3 additions & 0 deletions claude/commands/refresh-branch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
If this current branch has no associated remote branch, do nothing.

If it does have an associated remote branch, ensure that this branch is up to date with the remote branch, including force pulling and resetting the head of the current branch to match the remote.
7 changes: 5 additions & 2 deletions claude/hooks/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
TOKEN_USAGE_API_HOST="localhost:1234"
TOKEN_USAGE_API_TOKEN="abc123"
TOKEN_USAGE_API_HOST_DEV=https://localhost:3000
TOKEN_USAGE_API_TOKEN_DEV=abc123

TOKEN_USAGE_API_HOST_PROD=example.com
TOKEN_USAGE_API_TOKEN_PROD=abc123
9 changes: 9 additions & 0 deletions claude/hooks/run-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

if [ -x "bin/test.sh" ]; then
bin/test.sh
exit $?
else
echo "bin/test.sh not found or not executable. Each project should have a bin/test.sh script." >&2
exit 1
fi
110 changes: 73 additions & 37 deletions claude/hooks/send-usage-data.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,76 +21,112 @@ def load_env():

load_env()

API_HOST = os.environ.get("TOKEN_USAGE_API_HOST", "http://localhost:3000")
API_URL = f"{API_HOST.rstrip('/')}/token_usage"
API_TOKEN = os.environ.get("TOKEN_USAGE_API_TOKEN", "")
def get_last_message_id(session_id, host, api_token):
url = f"{host.rstrip('/')}/sessions/{session_id}/token_usages"
headers = {
"Authorization": f"Bearer {api_token}"
}
req = urllib.request.Request(url, headers=headers, method="GET")
with urllib.request.urlopen(req, timeout=30) as response:
data = json.loads(response.read().decode("utf-8"))
if data and len(data) > 0:
return data[0].get("message_id")
return None

def read_last_jsonl_entry(transcript_path):
last_line = None
def get_usage_data(transcript_path, after_message_id, session_id):
usage_data = {}
after_message_has_been_passed = after_message_id is None
with open(transcript_path, "r") as f:
for line in f:
line = line.strip()
if line:
last_line = line
if last_line:
return json.loads(last_line)
return None
entry = json.loads(line)
msg_id = entry.get("message", {}).get("id")

if msg_id == after_message_id:
after_message_has_been_passed = True
continue

if after_message_has_been_passed and entry.get("type") == "assistant":
usage_data[msg_id] = extract_usage_data(entry, session_id)

return list(usage_data.values())

def extract_usage_data(entry, session_id):
message = entry.get("message", {})
message_uuid = entry.get("uuid")

if not message_uuid:
return None
message_id = message.get("id")

usage = message.get("usage", {})
error = message.get("error", {})

return {
"session_id": session_id,
"message_uuid": message_uuid,
"message_id": message_id,
"input_tokens": usage.get("input_tokens", 0),
"output_tokens": usage.get("output_tokens", 0),
"error_type": error.get("type") if error else None,
"error_message": error.get("message") if error else None
}

def post_to_api(data):
payload = json.dumps({"datum": data}).encode("utf-8")
def post_usage_datum(datum, host, token):
url = f"{host.rstrip('/')}/token_usages"
payload = json.dumps({"token_usage": datum}).encode("utf-8")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_TOKEN}"
"Authorization": f"Bearer {token}"
}

req = urllib.request.Request(API_URL, data=payload, headers=headers, method="POST")
req = urllib.request.Request(url, data=payload, headers=headers, method="POST")

with urllib.request.urlopen(req, timeout=30) as response:
return response.status, response.read().decode("utf-8")

def main():
input_data = json.loads(sys.stdin.read())

transcript_path = input_data.get("transcript_path")
session_id = input_data.get("session_id")

if not transcript_path or not os.path.exists(transcript_path):
def send_usage_data(usage_data, host, token):
for usage_datum in usage_data:
message_id = usage_datum.get("message_id", "unknown")
try:
post_usage_datum(usage_datum, host, token)
except urllib.error.HTTPError as e:
print(f"Failed: {message_id} - {e.code} {e.reason}", file=sys.stderr)

def extract_and_send_usage_data_for_env(transcript_path, session_id, host, api_token):
try:
last_message_id = get_last_message_id(session_id, host, api_token)
except urllib.error.HTTPError as e:
print(f"Failed: {e.url} {e.code} - {e.reason}", file=sys.stderr)
return

if not session_id:
return
usage_data = get_usage_data(transcript_path, last_message_id, session_id)

if not API_TOKEN:
return
total_input = sum(e.get("input_tokens", 0) for e in usage_data)
total_output = sum(e.get("output_tokens", 0) for e in usage_data)
print(f"Found {len(usage_data)} unique usage data, total: input={total_input}, output={total_output}", file=sys.stderr)
send_usage_data(usage_data, host, api_token)

entry = read_last_jsonl_entry(transcript_path)
if not entry:
return
def main():
print("Sending usage data...", file=sys.stderr)

usage_data = extract_usage_data(entry, session_id)
if not usage_data:
return
input_data = json.loads(sys.stdin.read())

post_to_api(usage_data)
hook_event = input_data.get("hook_event_name")
if hook_event == "SubagentStop":
transcript_path = input_data.get("agent_transcript_path")
session_id = input_data.get("agent_id")
else:
transcript_path = input_data.get("transcript_path")
session_id = input_data.get("session_id")

dev_host = os.environ.get("TOKEN_USAGE_API_HOST_DEV", "")
dev_api_token = os.environ.get("TOKEN_USAGE_API_TOKEN_DEV", "")
if dev_host and dev_api_token:
print(f"Sending to Dev: {dev_host}", file=sys.stderr)
extract_and_send_usage_data_for_env(transcript_path, session_id, dev_host, dev_api_token)

prod_host = os.environ.get("TOKEN_USAGE_API_HOST_PROD", "")
prod_api_token = os.environ.get("TOKEN_USAGE_API_TOKEN_PROD", "")
print(f"Sending to Prod: {prod_host}", file=sys.stderr)
extract_and_send_usage_data_for_env(transcript_path, session_id, prod_host, prod_api_token)

sys.exit(1)

if __name__ == "__main__":
main()
53 changes: 34 additions & 19 deletions claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
{
"permissions": {
"allow": [
"Bash(git push:*)",
"Bash(git fetch:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git checkout:*)",
"Bash(git worktree:*)",
"Bash(git branch:*)",
"Bash(git rebase:*)",
"Bash(git:*)",
"Bash(command gh:*)",
"Bash(grep:*)",
"Bash(cat:*)",
Expand All @@ -18,12 +11,40 @@
"Bash(cp:*)",
"Bash(cd:*)",
"WebSearch",
"WebFetch",
"Bash(bundle install:*)",
"Bash(rails generate:*)"
"Bash(rails generate:*)",
"Bash(rspec:*)",
"Bash(bundle exec rspec:*)",
"Bash(bundle install)",
"Bash(rails db:*)",
"Bash(rubocop:*)",
"Bash(npm run dev)",
"Bash(npm dev)"
],
"deny": [
"Bash(bin/rails credentials/show)"
],
"deny": [],
"ask": [
"Bash(git merge:*)"
"Bash(git merge:*)",
"Bash(git push master)",
"Bash(git push main)",
"Bash(git push --force)",
"Bash(git push -f)",
"Bash(rm:*)",
"Bash(rails db:drop)",
"Bash(rails db:rollback)",
"Bash(rails db:reset)",
"Bash(rails db:migrate:redo)",
"Bash(rails db:migrate:down)",
"Bash(command gh repo archive:*)",
"Bash(command gh repo delete:*)",
"Bash(command gh repo edit:*)",
"Bash(command gh repo rename:*)",
"Bash(command gh pr merge:*)",
"Bash(command gh pr close:*)",
"Edit(Gemfile)",
"Write(Gemfile)"
],
"defaultMode": "default"
},
Expand All @@ -44,19 +65,15 @@
"matcher": "Write|Edit",
"hooks": [
{
"type": "prompt",
"prompt": "if any changes were made, run the tests to ensure that everything still works. Fix any thing that is broken."
"type": "command",
"command": "~/.claude/hooks/run-tests.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "If you changed any front end UI, go use Chrome and look at what you change and decide if it looks good. If it does not, update it until you think it looks good to present to the user."
},
{
"type": "command",
"command": "~/.claude/hooks/send-usage-data.py"
Expand Down Expand Up @@ -85,9 +102,7 @@
"feature-dev@claude-plugins-official": true,
"code-review@claude-plugins-official": true,
"commit-commands@claude-plugins-official": true,
"ralph-wiggum@claude-plugins-official": true,
"plugin-dev@claude-plugins-official": true,
"hookify@claude-plugins-official": true,
"agent-sdk-dev@claude-plugins-official": true,
"pr-review-toolkit@claude-plugins-official": true
}
Expand Down
Loading