IMPORTANT: Before starting any implementation or creating a plan, ask clarifying questions using the AskUserQuestion tool (not plain text). Always use the AskUserQuestion tool in Claude Code when you have questions to ask after a prompt. Use it to understand:
- The specific goals and desired outcomes
- Edge cases and error handling preferences
- UI/UX preferences (if applicable)
- Performance or scalability requirements
- Integration points with existing code
- Testing expectations
- Any constraints or preferences I might have
Codex: Use the request_user_input tool frequently throughout development - not just during planning.
Claude Code: Always use the AskUserQuestion tool (not plain text questions) when you need to ask the user something - during planning, implementation, and any other time.
Actively interview the user at any point (especially during planning). Prefer multiple rounds of short questions.
Asking questions is encouraged and appreciated because it:
- Helps both of us think through problems more clearly
- Surfaces edge cases and requirements that might be missed
- Leads to better solutions through collaborative dialogue
- Catches misunderstandings early before code is written
Ask about:
- Clarifying requirements and desired behavior
- UI/UX preferences and design decisions
- Trade-offs between different approaches
- Edge cases and error handling
- Whether a proposed solution matches expectations
- Anything you're uncertain about
Don't assume - ask. Multiple rounds of questions are better than one large batch. Even mid-implementation, if something feels unclear or you're choosing between options, ask. The interactive back-and-forth is valuable.
For debugging sessions: always take a screenshot first, reproduce the issue, then form a hypothesis before changing code. Do not start editing until the root cause is identified.
When I report a bug, don't start by trying to fix it. Instead, start by writing a test that reproduces the bug. Then, have subagents try to fix the bug and prove it with a passing test.
- iOS: See
clients/ios/CLAUDE.mdfor iOS simulator testing and development- All new iOS files must be written in Swift (not Objective-C)
- Android: Use the adb/emulator workflow below for emulator testing and screenshots
- Commit early and often for Android work: make frequent commits throughout implementation so there is a clear trail of changes; commit freely rather than batching large edits.
- Theme coverage is required for Android UI work: any Android UI change must be checked against all supported NewsBlur themes, not just the current device theme.
- Current required Android theme coverage:
light,dark, andblack. - Upcoming theme requirement: account for
Sapiain any new Android UI work so theme-specific colors are centralized and easy to extend when that theme lands.
- Prefer reusing an already running emulator instead of booting a new one
- Check attached devices with
adb devices -l - The current app package is
com.newsblur - The launcher activity resolves to
com.newsblur/.activity.InitActivity - Do not rely on
emulator -list-avdsbeing available onPATH; in this repo it was not
- Run Android Gradle commands from
clients/android/NewsBlur - Set
JAVA_HOMEexplicitly before Gradle commands or the build may pick the wrong JDK and fail withinvalid source release: 21 - Working command:
env JAVA_HOME=/opt/homebrew/opt/openjdk ./gradlew :app:installDebug - Fast compile-only verification:
env JAVA_HOME=/opt/homebrew/opt/openjdk ./gradlew :app:compileDebugJavaWithJavac --rerun-tasks - Reinstalling with
installDebugpreserves the existing logged-in emulator app state
- Launch the app with
adb -s emulator-5554 shell monkey -p com.newsblur -c android.intent.category.LAUNCHER 1 - Do not try
adb shell am start -n com.newsblur/com.newsblur.activity.Main;Mainis not exported and throws aSecurityException - Check the current foreground screen with
adb -s emulator-5554 shell dumpsys window | rg "mCurrentFocus|mFocusedApp" - Check whether the app process is alive with
adb -s emulator-5554 shell pidof com.newsblur - Resolve the launcher activity with
adb -s emulator-5554 shell cmd package resolve-activity --brief com.newsblur
- Save emulator screenshots with
adb -s emulator-5554 exec-out screencap -p > /tmp/newsblur-screenshot.png - View the screenshot from
/tmp/newsblur-screenshot.png - For cold-launch capture, force stop and relaunch, then capture multiple frames:
adb -s emulator-5554 shell am force-stop com.newsblur
adb -s emulator-5554 shell monkey -p com.newsblur -c android.intent.category.LAUNCHER 1
sleep 0.2
adb -s emulator-5554 exec-out screencap -p > /tmp/newsblur-launch-02.png- Pull to refresh from the feed list on the current 1080x2400 AVD with
adb -s emulator-5554 shell input swipe 540 600 540 1800 500 - Sanity check animation timing before UI verification:
adb -s emulator-5554 shell settings get global animator_duration_scaleadb -s emulator-5554 shell settings get global transition_animation_scaleadb -s emulator-5554 shell settings get global window_animation_scale- Expected value for real-speed verification is
1.0
- If sync UI is too fast to catch, throttle the emulator network:
adb -s emulator-5554 emu network speed gsmadb -s emulator-5554 emu network delay gprs
- Restore normal network speed afterward:
adb -s emulator-5554 emu network speed fulladb -s emulator-5554 emu network delay none
- Disable network to test offline behavior:
adb -s emulator-5554 shell svc wifi disableadb -s emulator-5554 shell svc data disable
- Re-enable network afterward:
adb -s emulator-5554 shell svc wifi enableadb -s emulator-5554 shell svc data enable
- Use git worktrees for parallel development: Run
make worktreein a worktree to start workspace-specific services - Main repo uses standard ports (80/443), worktrees get unique ports based on directory name hash
- Run
./worktree-dev.shto see your workspace's assigned ports (output shows all URLs) - Close worktree:
make worktree-closestops containers and removes worktree if clean (no uncommitted changes) - All worktrees share the same database services (postgres, mongo, redis, elasticsearch)
- Main repo containers:
newsblur_web,newsblur_celery,newsblur_node,newsblur_nginx,newsblur_haproxy - Worktree containers:
newsblur_web_<worktree-name>,newsblur_celery_<worktree-name>,newsblur_node_<worktree-name>,newsblur_nginx_<worktree-name>,newsblur_haproxy_<worktree-name> - Find worktree containers:
docker ps --format '{{.Names}}' | grep <worktree-name> - The worktree name is the directory name (e.g.,
search-by-phrase→newsblur_web_search-by-phrase)
make- Smart default: starts/updates NewsBlur, applies migrations (safe to run after git pull)make rebuild- Full rebuild with all images (for Docker config changes)make nb- Fast startup without rebuild (legacy, usemakeinstead)make bounce- Restart all containers with new imagesmake shell- Django shell inside containermake debug- Debug mode for pdbmake log- View logsmake lint- Run linting (isort, black, flake8)make test- Run all tests (defaults: SCOPE=apps, ARGS="--noinput -v 1 --failfast")make test SCOPE=apps.rss_feeds ARGS="-v 2"
IMPORTANT: Do NOT run make rebuild or make nb during development!
- Web and Node servers restart automatically when code changes
- Task/Celery server must be manually restarted when modifying any code that runs inside a Celery task — this includes the task file itself and any module it calls (e.g., scoring, summary, models). Without a restart, the worker keeps running the old code. Restart with:
docker restart newsblur_celery(ornewsblur_celery_<worktree-name>in worktrees) - Use
maketo apply migrations after git pull - Running
make rebuildunnecessarily rebuilds everything and wastes time
Note: All docker commands must use -t instead of -it to avoid interactive mode issues when running through Claude.
- Always run Python code and Django management commands inside the Docker container
- Do NOT use
uv runor local Python environment - always use the Docker container - Main repo:
docker exec -t newsblur_web python manage.py <command> - Worktree:
docker exec -t newsblur_web_<worktree-name> python manage.py <command> - Example (main):
docker exec -t newsblur_web python manage.py shell -c "<python code>" - Example (worktree):
docker exec -t newsblur_web_search-by-phrase python manage.py test apps
aps- Alias foransible-playbook ansible/setup.yml- Used only for setting up new servers or making global config changes (e.g., installing new packages). While it does deploy code changes, use apd for deploymentapd- Alias foransible-playbook ansible/deploy.yml- Used for regular code deployments. This is the command to run after merging a PR to main. It deploys code changes and restarts services without making global config changes.
Unless asked, don't run either of these. Assume I will deploy or ask you to deploy when ready.
To SSH into NewsBlur servers non-interactively:
./utils/ssh_hz.sh -n <server-name> "<command>"Example:
./utils/ssh_hz.sh -n happ-web-01 "hostname"
./utils/ssh_hz.sh -n hdb-redis-story-1 "redis-cli info stats"Server names are defined in ansible/inventories/hetzner.ini. Common server prefixes:
happ-- Application servers (web, refresh, count, push)hdb-- Database servers (redis, mongo, postgres, elasticsearch)htask-- Task/Celery workershnode-- Node.js services (page, favicons, text, socket, images)hwww- Main web serverhstaging- Staging server
-
Python:
- Black formatter with line-length 110
- Use isort with Black profile for imports
- Classes use CamelCase, functions/variables use snake_case
- Use explicit exception handling
- Follow Django conventions for models/views
-
JavaScript:
- Use snake_case for methods and variables (not camelCase)
- Framework: Backbone.js with jQuery/Underscore.js
-
Tests:
- Classes prefixed with
Test_ - Methods prefixed with
test_
- Classes prefixed with
-
Prioritize readability over performance
-
Leave no TODOs or placeholders
-
Always reference file names in comments
- Posts live in
blog/_posts/, drafts inblog/_drafts/ blog/_site/contains generated output and must be committed — it's how the blog gets deployed
- Test API endpoints:
make api URL=/reader/feeds - With POST data:
make api URL=/reader/river_stories ARGS="-X POST -d 'feeds[]=1&feeds[]=2&feeds[]=3'"
- Restart Celery after changes: Ask AI questions are processed asynchronously via Celery tasks. After modifying
apps/ask_ai/(providers, tasks, models), restart celery:docker restart newsblur_celery(ornewsblur_celery_<worktree-name>in worktrees) - Provider implementations are in
apps/ask_ai/providers.py - Frontend model selectors: Search for
data-model="geminito find all dropdown locations - CSS for model pills: Search for
NB-provider-to find pill styles
- Projects:
web,task,node,monitor(auth token in~/.sentryclirc) - Always use sentry-cli when given a Sentry issue URL
- Extract issue ID from URL:
https://sentry.newsblur.com/organizations/newsblur/issues/1037/→ issue ID is1037
# List unresolved issues
sentry-cli --url https://sentry.newsblur.com issues list -o newsblur -p web --status unresolved
# Get specific issue with full details (use --log-level debug to see JSON with file/function info)
sentry-cli --url https://sentry.newsblur.com issues list -o newsblur -p web --query "issue.id:1037" --log-level debug 2>&1 | grep "body:"
# The JSON body contains: culprit (endpoint), metadata.filename, metadata.function
# Example output: "culprit":"/profile/save_ios_receipt/","metadata":{"filename":"apps/profile/models.py","function":"setup_premium_history",...}
# List issues for other projects (task, node, monitor)
sentry-cli --url https://sentry.newsblur.com issues list -o newsblur -p task --status unresolved
# Resolve an issue after fixing (use issue ID from URL)
sentry-cli --url https://sentry.newsblur.com issues resolve -o newsblur -p web -i 1037- Extract issue ID from URL (e.g.,
.../issues/1037/→1037) - Get issue details with
--log-level debugto find the file and function - Fix the issue in code
- Commit the fix
- Always resolve the issue on Sentry with
sentry-cli issues resolve -i <issue_id>— do not skip this step
- Do NOT use the Chrome DevTools MCP server unless explicitly asked — the user will verify manually
- Local dev:
https://localhostfor main repo. In a worktree, runmake worktreefirst, then./worktree-dev.shto get the assigned ports/URLs. - Screenshots: Save to
/tmp/newsblur-screenshot.png, then use Read tool to view
https://localhost/reader/dev/autologin/- Login as default dev user (configured inDEV_AUTOLOGIN_USERNAME)https://localhost/reader/dev/autologin/<username>/- Login as specific user- Add
?next=/pathto redirect after login - Returns 403 Forbidden in production (DEBUG=False)
?test=growth- Test growth prompts (bypasses premium check and cooldowns)?test=growth1- Test feed_added growth prompt?test=growth2- Test stories_read growth prompt
NEWSBLUR.reader.switch_theme('dark')- Switch to dark modeNEWSBLUR.reader.switch_theme('light')- Switch to light modeNEWSBLUR.reader.switch_theme('auto')- Switch to auto/system theme
NEWSBLUR.reader.open_premium_upgrade_modal()- Premium upgrade dialogNEWSBLUR.reader.open_feedchooser_modal()- Feed chooser (mute sites)NEWSBLUR.reader.open_account_modal()- Account settingsNEWSBLUR.reader.open_preferences_modal()- PreferencesNEWSBLUR.reader.open_keyboard_shortcuts_modal()- Keyboard shortcutsNEWSBLUR.reader.open_goodies_modal()- Goodies & appsNEWSBLUR.reader.open_notifications_modal(feed_id)- Notifications for feedNEWSBLUR.reader.open_newsletters_modal()- Email newslettersNEWSBLUR.reader.open_organizer_modal()- Organize feedsNEWSBLUR.reader.open_trainer_modal()- Intelligence trainerNEWSBLUR.reader.open_add_feed_modal()- Add new feedNEWSBLUR.reader.open_friends_modal()- Find friendsNEWSBLUR.reader.open_intro_modal()- Intro/tutorialNEWSBLUR.reader.open_feed_statistics_modal(feed_id)- Feed statisticsNEWSBLUR.reader.open_feed_exception_modal(feed_id)- Feed exceptionsNEWSBLUR.reader.open_mark_read_modal()- Mark as read optionsNEWSBLUR.reader.open_social_profile_modal(user_id)- Social profile$.modal.close()- Close any open modal
NEWSBLUR.reader.open_river_stories()- Open All Site StoriesNEWSBLUR.reader.open_feed(feed_id)- Open a specific feedNEWSBLUR.assets.feeds.find(f => f.get('nt') > 0)- Get feed with unread storiesNEWSBLUR.assets.feeds- Backbone.js collection of all feeds
.NB-feed-story- Select first story.NB-feed-story-train- Open story intelligence trainer.NB-feedbar-options- Open feed options popover.folder .folder_title- Open folder
To test different subscription states, modify user profile in Django shell:
docker exec -t newsblur_web_<worktree> python manage.py shell -c "
from apps.profile.models import Profile
p = Profile.objects.get(user__username='<username>')
p.is_premium = True # Enable premium
p.is_premium_trial = True # Set as trial (False = paid)
p.is_archive = False # Archive tier
p.is_pro = False # Pro tier
p.premium_renewal = True # Has active renewal
p.save()
"- DNS/Service Discovery: Docker containers resolve services via dnsmasq → Consul (e.g.,
redis-story.service.consul) - Container Names by Server Type:
- Web (
happ-web-*):newsblur_web,haproxy - Task (
htask-celery-*):task-celery,autoheal - Task (
htask-work-*):task-work,autoheal - Node (
hnode-page/text/socket/favicons-*):node - Node (
hnode-images-*):imageproxy - Redis (
hdb-redis-{story,user,session,pubsub}-*):redis-story,redis-user,redis-session,redis-pubsub - Mongo (
hdb-mongo-*):mongo - Postgres (
hdb-postgres-*):postgres - Nginx (
hwww):nginx,haproxy
- Web (
When the user reports downtime, read DOWNTIME.md for the full investigation playbook, then run ./utils/check_health.sh.
When load times are elevated but the site isn't down, read SLOW_LOAD_TIMES.md for the full investigation playbook. Start with the Quick Assessment commands to check per-minute and hourly load times.
- Never use em dashes or hyphens as punctuation. Restructure the sentence instead.
- Sign off with just "Sam" (no "Best," "Thanks," or other closings before it)
- Keep it concise and direct
- Keep replies free of AI slop: no em dashes, no hyphens as punctuation, no overly polished language. Restructure sentences instead.
- Be concise and direct, matching a natural conversational tone