One of Ida's skills
People Say
A skill that researches anything by finding what people actually say about it: it searches what real people say to each other in forums and comment threads, and ignores everything else, especially marketing. Launching it looks like typing /people-say best ramen in downtown Brooklyn, or just asking "what do people think about X?"
Most of this page, and the skill it documents, was written by AI, specifically Claude Opus 4.8 and 5. People Say is a skill Ida's AI wrote and revises from her feedback; the same AI compiled this page from the skill's files, and Ida edited it. The essay it belongs to, Obsessing Over AI Agents Is Slowing You Down, is entirely her writing.
The Skill Files
The whole skill is five files: one instruction file that tells the AI what to do, and four small scripts it can run. What you see here is a snapshot; the copy on Ida's machine keeps changing as the skill improves. One file path is blacked out because it points at where login credentials would live on her machine.
▶
people-say/SKILL.md
The complete skill file. Click to read it here.
Download
---
name: people-say
description: Research anything — products, tools, software, equipment, services, vendors, local businesses, restaurants, venues, classes, books, techniques, frameworks, or concepts — by finding organic community discussion rather than SEO-optimized content. Use when evaluating, comparing, or deciding between options. Triggers include "what do people think about," "is X any good," "compare X and Y," "what should I use for," "has anyone used," "what are alternatives to," "look into," "reviews of," or any request to evaluate or research something before buying, hiring, or adopting it.
---
# People Say
Researches any topic by prioritizing organic community discussion over marketing content and review aggregators. Use when evaluating tools, services, vendors, local businesses, approaches, or concepts.
## Invocation
`/people-say [topic]` or `/people-say [topic] -- [context or specific questions]`
Examples:
- `/people-say BoldSign esignature`
- `/people-say Creality K1C -- reliability, first printer experience`
- `/people-say community facilitation frameworks -- change management for small teams`
- `/people-say best ramen in downtown Brooklyn`
---
## Setup
Reddit requires an authenticated session as of 2026 — unauthenticated requests get a "blocked by network security" interstitial regardless of IP, VPN, or User-Agent.
**Default auth: browser cookies (no setup).** The Reddit tools read `reddit_session` and related cookies directly from Brave or Safari via `browser_cookie3`. As long as you stay logged into reddit.com in one of those browsers, the scripts work. Cookies rotate every few weeks; you'll only need to re-log-in if requests start returning 401.
The one dependency: `pip3 install --user --break-system-packages browser_cookie3` (already installed on the M5).
**Fallback auth: OAuth script app (optional).** Useful if you want headless operation, or if the cookie path stops working. Reddit's developer-app creation page has a long-running reCAPTCHA bug that many users hit — if app creation works for you, great; if not, stick with cookies. To set up:
1. Sign in at https://www.reddit.com/prefs/apps and click **create another app**.
2. Choose **script** type. Name: `people-say-research`. Redirect URI: `http://localhost:8080`.
3. Create `[redacted — local config path]` with `REDDIT_CLIENT_ID`, `REDDIT_CLIENT_SECRET`, `REDDIT_USERNAME`, `REDDIT_PASSWORD`.
4. `chmod 600 [redacted — local config path]`
The auth helper tries cookies first, OAuth second, and errors out clearly if neither is available.
---
## Step 1: Classify the Research Type
Determine which category the topic falls into. This shapes where to search and what to look for.
| Type | What You're Evaluating | Key Questions |
|------|----------------------|---------------|
| **Product/Tool** | Software, hardware, equipment, materials | Who uses this? Real complaints? What do people switch to/from? |
| **Service/Vendor** | A company or provider you might hire or buy from | Reputation? Reliability? Red flags? Hidden costs? |
| **Local Business** | Brick-and-mortar, location-specific | Quality? Consistency? What regulars say vs. one-time visitors? |
| **Concept/Approach** | Method, framework, technique, practice | Do practitioners actually use this? What are the limits? What replaced it? |
| **Person/Organization** | Public figure, company, institution | What do peers say? Track record? Gaps between reputation and reality? |
---
## Step 2: Search Organic Discussion Sources
### Start with Reddit (Tier 1a):
- **Reddit** — broadest coverage, almost always the richest source of organic discussion. Run the local scripts in `tools/` (see **Local Tools** below) **before any other searches**. Auth is automatic via browser cookies — no setup needed as long as Ida is logged into reddit.com in Brave or Safari. **WebFetch cannot access Reddit at all** — it refuses outright — and `site:reddit.com` in WebSearch returns zero Reddit URLs (Google's exclusivity deal pulls Reddit out of other engines' indexes). The local scripts are the only working path.
If the scripts fail with a "no auth available" error, ask Ida to log into reddit.com in Brave or Safari, then retry. If they fail with 401, the browser session expired — same fix. Do not waste effort on WebFetch or WebSearch fallbacks for Reddit content; they do not work in this environment.
### Then search remaining Tier 1 sources (run in parallel):
- **Hacker News** — `site:news.ycombinator.com [topic]`. Strong for tech, business, tools, but also has practitioners in many fields.
- **Bluesky** — growing community with arts, culture, media, design, and academic voices. Use `tools/bluesky_search.py "query"` for direct AppView access (no auth required). Useful query operators: `from:handle.bsky.social`, `lang:en`, exact-quote `"phrase"`, `--since 2025-01-01` for date-bounded recent discussion. Bluesky's strength is real-name practitioners posting under professional handles — quality is higher signal-to-noise than Twitter circa 2020, weaker on broad consumer products.
- **GitHub Issues** — for software/developer tools, fetch the repo's issues page directly: `github.com/[org]/[repo]/issues?q=[search terms]`. Contains real user feedback, bug reports, and feature requests. **Important:** Fetch the GitHub search URL directly rather than using web search — web search has indexing lag and may miss recent issues.
### Then search for domain-specific communities (Tier 2):
Do NOT rely on a hardcoded list. Instead, discover where practitioners gather:
1. Search: `[topic] forum`, `[topic] community`, `[domain] discussion board`
2. Search: `"[topic]" site:forum.*` or `"[topic]" site:community.*`
3. Look for subreddit discovery: `reddit [domain] subreddit`
4. Check if the topic has a Discord, Slack, or Are.na community with public-facing content
**Examples of domain-specific communities that might surface:**
- Fiber/textile arts → Ravelry, craft subreddits, weaving forums
- Finance/investing → Bogleheads, r/personalfinance
- Design/creative → Are.na, Designer News
- Open source/dev tools → GitHub Issues/Discussions, Lobsters, Stack Overflow
- NYC local → neighborhood subreddits (r/brooklyn, r/nyc), Google Reviews, Yelp (for brick-and-mortar)
- Education → discipline-specific forums, Chronicle
- Nonprofit/facilitation → specific LinkedIn groups (note: these skew performative)
- Music/audio → Gearslutz (now Gearspace), specific genre forums
The goal is to find the equivalent of "where do people who actually do this thing talk to each other?"
### Use for secondary confirmation only (Tier 3 — discount heavily):
- **G2, Capterra, Trustpilot, Software Advice** — reviews are often incentivized. Use only to confirm patterns already found in organic discussion.
- **Listicle/comparison sites** ("Top 10 X alternatives") — usually affiliate-driven. Ignore rankings; only useful for discovering options you haven't heard of.
- **Company blogs** comparing themselves to competitors — marketing, not research.
- **Sponsored content / "partnered" reviews** — discard entirely.
---
## Step 3: Interpret What You Find
### Presence signals
- **Multiple independent mentions** across different communities = strong signal
- **Detailed experience reports** (not just "I like it") = high value
- **People recommending it unprompted** in "what do you use?" threads = strongest signal
- **Migration stories** ("I switched from X to Y because...") = reveals real strengths/weaknesses
### Absence signals
- **No organic discussion at all** = significant negative signal — but only if you actually checked the places where it would live. A product/service that nobody talks about across forums you searched is likely very new, very niche, or not good enough to generate word-of-mouth. State this explicitly.
- **Distinguish "nobody talks" from "we can't see."** If Reddit was unavailable (OAuth not configured, scripts failed, rate-limited), Reddit silence is not evidence — say "Reddit not searched; community signal incomplete." Same for any other source that errored. Absence is only a real signal when you successfully queried the source and found nothing.
- **Discussion only on review aggregators** = likely astroturfed or incentivized. Flag this.
- **Only the company itself posting** (Show HN with no comments, reddit posts from brand accounts with no engagement) = no community validation.
### Red flags
- Reviews that all sound similar or were posted in a short time window
- Overly positive language without specifics
- Review profiles that only review one product
- Company responses that deflect rather than address complaints
---
## Step 3.5: Verify Source Integrity
Before reporting findings, check your own work. This step catches inflated confidence and misattributed community signal.
### Organic vs. Planted
For each community source cited, verify it's actually organic discussion. Check: Is the poster a real community participant (history of other posts)? Is the thread responding to a brand prompt or a genuine user question? If you can't confirm, downgrade to Tier 2.
### Quote Accuracy
If you're quoting or paraphrasing a community post, confirm the quote is from the page you fetched, not reconstructed from memory. If you didn't fetch the actual thread, say "reported sentiment" not "users say."
### Thin-Evidence Honesty
Count your actual independent organic sources. If fewer than 3, the Source Quality Assessment must say "thin" regardless of how confident the pattern feels. Do not let 2 Reddit comments and a review aggregator become "moderate discussion."
### Quantify Per Option, Not Just Overall
Total source count is not enough — tally the signal *for each specific recommendation and each specific complaint*. For every option you report, state how many distinct people endorsed it and across how many threads (e.g. "~6 people across 3 threads," not "several people"). Same for negatives: how many distinct people raised the concern. The numbers change the decision — "one person named this exact model" and "six people independently recommended this brand" are different strengths of evidence and must not be reported at the same altitude. Watch the specificity level too: a brand getting 6 mentions does NOT mean a specific model/version got 6 — count at the granularity the user is actually deciding on (brand vs. model, product vs. tier). When a count is small (1–2), say the raw number, not a word like "several" that rounds it up.
### Close-Enough Trap
Confirm each cited thread is about the same thing the user is evaluating — same product version, same use case, same market segment, same geography. A thread about the enterprise tier is not evidence about the free plan. A 2023 complaint may not reflect the current product. If the match is approximate, say so.
### Alternatives Sourced vs. Inferred
The "Alternatives Mentioned" section should only contain alternatives that were actually recommended by community members. If supplementing with your own knowledge, label it clearly: "Also worth knowing (not community-sourced)."
### Alternative-Framing Stress Test
Before finalizing, ask: if the question had been framed narrower, wider, or with a different assumption, would different sources show up? Run one quick contrarian search. A tool rated "well-loved" in general use may not hold for the user's specific case (enterprise vs. personal, beginner vs. expert, free vs. paid tier). Note where findings are scope-dependent rather than universal.
### Access vs. Scope Honesty
"Inaccessible" means genuinely beyond reach (paywall, login wall, no transcript, platform block). "Surfaced but not pursued" means you saw the source and chose to stop. Filing scoping decisions as access limits inflates apparent coverage and hides your own calls. Before finalizing, walk every source you flagged as inaccessible and confirm you actually tried — if a WebFetch or `gh` call would have worked, it belongs in "Surfaced but not pursued."
---
## Step 3.6: Output Gate — Facts Plus Open Questions
The final gate before delivery. Step 3.5 checks source integrity; this one catches verdicts and rankings Ida didn't ask for.
**Research is facts plus open questions.** Scan the assembled findings for: recommendations or verdicts Ida didn't ask for; options ranked by fit; findings mapped onto her decision; decision-frameworks ("if X, then choose Y"). Cut each, or recast it as an open question. Tensions stay observations, not resolved forks. Note the count in "What Practitioners Say" is evidence, not a verdict — "~6 people across 3 threads recommended X" is a fact; "so X is the best choice" is a verdict.
**Exception:** Ida explicitly asked for a read or recommendation — "what should I use for," "compare X and Y," "is X any good" often carry that ask directly. When she did, give the read and mark it as such. When she only asked what people say, keep it to what people say.
**End the delivery with a fixed-prefix line** so the gate leaves a countable artifact (`chat_log_scan.py` counts lines beginning `Open-ended:`). Place it as the last line of the output, after the Sources section:
- `Open-ended: check` — nothing to recast.
- `Open-ended: recast N verdicts as open questions` — N items cut or recast.
---
## Step 4: Report Findings
Structure the output as:
### Summary
One paragraph: what the research found, confidence level, and whether organic discussion supports or contradicts marketing claims.
### What Practitioners Say
Organized by theme (not by source). Include direct quotes where available. Note which community each insight came from. **Attach a count to each recommendation and each concern** — how many distinct people, across how many threads (e.g. "CHI — ~6 people across 3 threads; only 1 named the specific model"). Count at the granularity the user is deciding on (brand vs. exact model). Use raw numbers for small counts (say "1 person," not "several"). If counts come only from the threads you actually fetched (not all of the platform), say so — these are floor numbers, not totals.
### Concerns & Red Flags
Anything negative, including absence of discussion. Be direct.
### Podcast / Audio Conversations
Surface any podcast episodes, interviews, or talks where practitioners discuss the topic in dialogue. Ida learns exceptionally well through audio — a good podcast conversation can be more useful than a written review. Search for these explicitly when relevant.
### Source Quality Assessment
- How much organic discussion exists (none / thin / moderate / substantial)
- Whether review aggregator sentiment matches or diverges from organic discussion
- Any signs of astroturfing or incentivized reviews
### Alternatives Mentioned
What do people in these communities actually recommend instead? This is often more valuable than the direct research.
### Inaccessible
Sources you tried to reach or that obviously hold relevant material but are genuinely beyond reach this run — login walls, paywalls, video-only content with no transcript, platform blocks, anti-automation measures. Name what + why. Do NOT use this section for sources you simply didn't search.
### Surfaced but not pursued
Sources you noticed during the research and made a deliberate call not to drill into — adjacent communities, foreign-language threads, GitHub repos, Substacks, secondary leads. Name what + one-line why (had enough signal, marginal value, scope choice). This gives the reader leads for a deeper pass and labels your scoping decisions honestly.
### Sources
List all sources as markdown links, grouped by tier:
- **Community discussion:** [links]
- **Review aggregators (secondary):** [links]
- **Other:** [links]
---
## Step 5: Reflect and Propose (only if something concrete surfaced)
After delivering the report from Step 4, evaluate whether the run surfaced anything that suggests a skill improvement.
**Default behavior: skip this step silently.** Only fire if there is a specific, concrete observation from this run that maps to a specific, concrete edit. Vague feelings of "this could be better" do not count. Manufactured observations to justify firing the step are worse than skipping.
**Things that warrant firing:**
- A failure or near-failure during the run (wrong output, missed step, instructions that didn't apply cleanly)
- Friction that suggests an instruction is ambiguous or missing
- A new failure mode the existing Gotchas don't cover
- Documentation drift (e.g., a tool flag in the docs that no longer works)
**If something concrete surfaced:**
1. **Cross-reference Claude's Vault Notebook** (`~~ Areas/Flight Deck/Claude's Vault Notebook.md`). Search for related entries by failure family, by skill name, and by topic. Count related entries and note recency.
2. **Present in chat using this shape:**
- **Observed:** What happened (one sentence)
- **Possible update:** Specific proposed edit (concrete edit text, not vague suggestion)
- **Related notebook entries:** None / single instance / 2 related / 3+ related (pattern threshold)
- **Recommendation:** Apply now / log as notebook observation only / hold for vault sweep / drop
3. **Ask Ida:** Approve / modify / skip / defer.
4. **If approved:**
- Skill edits: apply directly to `skill.md`
- Notebook entries: write a dated observation matching existing format and density
- Both, if both are warranted
5. **One observation per turn.** If multiple things surfaced, present the highest-confidence one first; queue the rest.
6. **Don't fill in if Ida is uncertain.** Deferred observations stay as candidates for the vault sweep.
---
## Important Principles
- **Absence is data.** If nobody is talking about something, say so. Don't fill the gap with review aggregator content.
- **Err toward skepticism.** Marketing claims are not evidence. Incentivized reviews are not evidence. Only organic practitioner discussion counts as primary evidence.
- **State confidence levels.** If the research is thin, say "low confidence — limited organic discussion found." Don't present weak evidence with false authority.
- **Don't conflate popularity with quality.** A tool with massive marketing spend may have less organic discussion than a smaller tool people actually love.
- **Context matters.** A tool that's great for enterprise may be terrible for a freelancer. Always evaluate relative to the user's actual situation and scale.
- **Search iteratively.** If initial searches are thin, try synonyms, adjacent terms, the founder's name, the parent company, or the problem the tool solves rather than the tool name itself.
---
## Local Tools
Scripts in `tools/` for direct access to sources that don't index well in WebSearch or are outright blocked from WebFetch.
### Reddit (browser-cookie auth by default; see Setup)
- `tools/reddit_search.py` — search Reddit or a specific subreddit
```
python3 tools/reddit_search.py "search query" --limit 10 --sort relevance
python3 tools/reddit_search.py "search query" --subreddit AskAnthropology
```
- `tools/reddit_thread.py` — fetch a thread with full comment tree
```
python3 tools/reddit_thread.py "https://reddit.com/r/sub/comments/id/title/" --depth 4 --comment-limit 50
```
**Failure modes and what to do:**
- **Exit code 2 ("No Reddit auth available")** — neither browser cookies nor OAuth env file produced valid auth. Most likely Ida is logged out of reddit.com in both Brave and Safari, or the cookie database is locked by an open browser session. Ask her to confirm she's logged into reddit.com in Brave, then retry.
- **401 ("auth expired or rejected")** — browser session cookies rotated or OAuth token expired. For cookies: re-log-in at reddit.com in Brave. For OAuth: delete `[redacted — local config path]` and retry.
- **403 ("session flagged or subreddit private")** — the account may have been rate-limited or the subreddit is private/quarantined. Wait a few minutes and retry, or check the subreddit directly in the browser.
- **Connection timeout / connection reset / SSL failure** — network layer, not identity. Cookie auth tested working with Mullvad on (2026-06-14), so VPN is no longer the first suspect. Check basic network connectivity first; only ask Ida to toggle VPN if other diagnostics come up clean.
**Rate limiting:** Authenticated script apps get ~60 req/min. Typical research (1 search + 2-5 thread fetches) is well under. Token is cached for ~1 hour so back-to-back research sessions don't re-auth each time.
**When to use:** Run `reddit_search.py` as the **first step** of Tier 1 research when credentials are configured. After search results come back, run `reddit_thread.py` on the 2-3 most relevant threads. If credentials are not configured, skip Reddit and proceed with other Tier 1 sources — note the gap in the final report rather than pretending the absence of Reddit findings is signal.
### Bluesky (no auth)
- `tools/bluesky_search.py` — search Bluesky posts via the unauthenticated AppView
```
python3 tools/bluesky_search.py "search query" --limit 25 --sort top
python3 tools/bluesky_search.py "M5 MacBook Pro" --since 2026-01-01 --lang en
```
Hits `api.bsky.app/xrpc/app.bsky.feed.searchPosts` directly. No keys, no rate-limit friction at research volumes. Query syntax supports `from:handle.bsky.social`, exact-quote `"phrase"`, `lang:en`, and date filters via `--since`/`--until`. Top-sort is best for general signal; latest-sort is better for tracking ongoing discussion. Use this in parallel with Reddit and HN in Tier 1.
▶
people-say/tools/
The four scripts, one after another. Click to read them here.
Download all
▶tools/reddit_search.pysearches Reddit, or one subreddit
#!/usr/bin/env python3
"""Search Reddit using whichever auth path is configured (cookies or OAuth).
See _reddit_auth.py for the auth resolution order. By default, reads your
browser's Reddit session cookies — no setup needed beyond being logged in.
Usage:
python3 reddit_search.py "search query" [--subreddit NAME] [--limit N] [--sort relevance|hot|new|top]
"""
import argparse
import sys
from _reddit_auth import get_client
def search(client, query, subreddit=None, limit=10, sort="relevance"):
if subreddit:
path = f"/r/{subreddit}/search.json"
params = {"q": query, "limit": limit, "sort": sort, "restrict_sr": "on", "raw_json": 1}
else:
path = "/search.json"
params = {"q": query, "limit": limit, "sort": sort, "raw_json": 1}
resp = client.get(path, params=params, timeout=15)
if resp.status_code == 401:
print("BLOCKED: 401 from Reddit. If using cookies, your browser session "
"expired — log into reddit.com in Brave/Safari and retry.",
file=sys.stderr)
sys.exit(1)
if resp.status_code == 403:
print("BLOCKED: 403 from Reddit. Session may be flagged, or subreddit "
"is private/quarantined.", file=sys.stderr)
sys.exit(1)
resp.raise_for_status()
data = resp.json()
results = []
for post in data.get("data", {}).get("children", []):
d = post["data"]
results.append({
"subreddit": d.get("subreddit", ""),
"title": d.get("title", ""),
"score": d.get("score", 0),
"comments": d.get("num_comments", 0),
"url": f"https://reddit.com{d.get('permalink', '')}",
"created": d.get("created_utc", 0),
"selftext_preview": (d.get("selftext", "") or "")[:200],
})
return results
def main():
parser = argparse.ArgumentParser(description="Search Reddit")
parser.add_argument("query", help="Search terms")
parser.add_argument("--subreddit", "-s", help="Limit to a subreddit")
parser.add_argument("--limit", "-l", type=int, default=10, help="Max results (default 10)")
parser.add_argument("--sort", choices=["relevance", "hot", "new", "top"], default="relevance")
args = parser.parse_args()
client = get_client()
results = search(client, args.query, args.subreddit, args.limit, args.sort)
if not results:
print("No results found.")
return
for i, r in enumerate(results, 1):
print(f"{i}. r/{r['subreddit']} | Score: {r['score']} | Comments: {r['comments']}")
print(f" {r['title']}")
if r["selftext_preview"]:
preview = r["selftext_preview"].replace("\n", " ").strip()
print(f" {preview}...")
print(f" {r['url']}")
print()
if __name__ == "__main__":
main()
▶tools/reddit_thread.pypulls a full thread with its comments
#!/usr/bin/env python3
"""Fetch a Reddit thread with comments via whichever auth path is configured.
See _reddit_auth.py for the auth resolution order. By default, reads your
browser's Reddit session cookies — no setup needed beyond being logged in.
Usage:
python3 reddit_thread.py URL [--depth N] [--comment-limit N]
"""
import argparse
import sys
from _reddit_auth import get_client, url_to_api_path
def fetch_thread(client, url, comment_limit=50, depth=4):
path = url_to_api_path(url)
if not path:
print(f"ERROR: Could not parse Reddit thread URL: {url}", file=sys.stderr)
sys.exit(1)
path = path + ".json"
params = {"limit": comment_limit, "depth": depth, "raw_json": 1}
resp = client.get(path, params=params, timeout=30)
if resp.status_code == 401:
print("BLOCKED: 401 from Reddit. If using cookies, your browser session "
"expired — log into reddit.com in Brave/Safari and retry.",
file=sys.stderr)
sys.exit(1)
if resp.status_code == 403:
print("BLOCKED: 403 from Reddit. Subreddit may be private/quarantined.",
file=sys.stderr)
sys.exit(1)
resp.raise_for_status()
data = resp.json()
if not isinstance(data, list) or len(data) < 2:
print("Unexpected response format.", file=sys.stderr)
sys.exit(1)
post_data = data[0]["data"]["children"][0]["data"]
comments_data = data[1]["data"]["children"]
return post_data, comments_data
def format_post(post):
lines = []
lines.append(f"# {post.get('title', '[no title]')}")
lines.append(
f"r/{post.get('subreddit', '?')} | u/{post.get('author', '[deleted]')} | "
f"Score: {post.get('score', 0)} | Comments: {post.get('num_comments', 0)}"
)
lines.append("")
selftext = post.get("selftext", "")
if selftext:
lines.append(selftext)
lines.append("")
if post.get("url") and not post.get("is_self"):
lines.append(f"Link: {post['url']}")
lines.append("")
return "\n".join(lines)
def format_comments(children, depth=0):
lines = []
indent = " " * depth
for item in children:
if item.get("kind") != "t1":
continue
c = item.get("data", {})
author = c.get("author", "[deleted]")
score = c.get("score", 0)
body = c.get("body", "[removed]")
lines.append(f"{indent}**u/{author}** (score: {score})")
for line in body.split("\n"):
lines.append(f"{indent}{line}")
lines.append("")
replies = c.get("replies")
if isinstance(replies, dict):
reply_children = replies.get("data", {}).get("children", [])
if reply_children:
lines.extend(format_comments(reply_children, depth + 1))
return lines
def main():
parser = argparse.ArgumentParser(description="Fetch a Reddit thread")
parser.add_argument("url", help="Reddit thread URL")
parser.add_argument("--depth", "-d", type=int, default=4, help="Comment nesting depth (default 4)")
parser.add_argument("--comment-limit", "-c", type=int, default=50, help="Max top-level comments (default 50)")
args = parser.parse_args()
client = get_client()
post, comments = fetch_thread(client, args.url, args.comment_limit, args.depth)
print(format_post(post))
print("---")
print("## Comments")
print()
comment_lines = format_comments(comments)
print("\n".join(comment_lines))
if __name__ == "__main__":
main()
▶tools/bluesky_search.pysearches Bluesky posts
#!/usr/bin/env python3
"""Search Bluesky via the unauthenticated AppView API.
Uses api.bsky.app/xrpc/app.bsky.feed.searchPosts — no auth, no keys, no
rate-limit friction at typical research volumes. The public.api.bsky.app
mirror is behind a CDN that blocks bare User-Agents; api.bsky.app is open.
Usage:
python3 bluesky_search.py "search query" [--limit N] [--sort top|latest]
python3 bluesky_search.py "search query" --since 2026-01-01 --lang en
"""
import argparse
import sys
import requests
API = "https://api.bsky.app/xrpc/app.bsky.feed.searchPosts"
HEADERS = {
"User-Agent": "people-say:v2.0 (research tool)",
"Accept": "application/json",
}
def search(query, limit=25, sort="top", since=None, until=None, lang=None):
params = {"q": query, "limit": min(limit, 100), "sort": sort}
if since:
# API requires full ISO 8601 datetime; pad bare YYYY-MM-DD to start-of-day UTC
params["since"] = since if "T" in since else f"{since}T00:00:00Z"
params["sort"] = "latest" # date filters require latest sort
if until:
params["until"] = until if "T" in until else f"{until}T23:59:59Z"
params["sort"] = "latest"
if lang:
params["lang"] = lang
resp = requests.get(API, params=params, headers=HEADERS, timeout=15)
resp.raise_for_status()
return resp.json().get("posts", [])
def format_post(p):
handle = p["author"]["handle"]
display = p["author"].get("displayName") or handle
text = p["record"].get("text", "")
likes = p.get("likeCount", 0)
reposts = p.get("repostCount", 0)
replies = p.get("replyCount", 0)
rkey = p["uri"].rsplit("/", 1)[-1]
url = f"https://bsky.app/profile/{handle}/post/{rkey}"
created = p["record"].get("createdAt", "")[:10]
lines = [
f"@{handle} ({display}) | {created}",
f" likes: {likes} | reposts: {reposts} | replies: {replies}",
]
for line in text.split("\n"):
lines.append(f" {line}")
lines.append(f" {url}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Search Bluesky posts")
parser.add_argument("query", help="Search terms (supports quotes, from:handle, lang:en, etc.)")
parser.add_argument("--limit", "-l", type=int, default=25, help="Max results (default 25, capped at 100)")
parser.add_argument("--sort", choices=["top", "latest"], default="top")
parser.add_argument("--since", help="ISO date (forces --sort latest)")
parser.add_argument("--until", help="ISO date (forces --sort latest)")
parser.add_argument("--lang", help="Two-letter language code (e.g. en)")
args = parser.parse_args()
posts = search(args.query, args.limit, args.sort, args.since, args.until, args.lang)
if not posts:
print("No results found.")
return
for i, p in enumerate(posts, 1):
print(f"{i}.")
print(format_post(p))
print()
if __name__ == "__main__":
main()
▶tools/_reddit_auth.pyhandles Reddit login via browser cookies
"""Reddit auth helper for people-say tools.
Two auth paths, tried in order:
1. Browser cookie session (default; no setup) — reads `reddit_session` and
related cookies from Brave or Safari via browser_cookie3. Works as long as
you're logged into Reddit in the browser. Cookies rotate every few weeks;
no action needed unless requests start 401'ing.
2. OAuth script app (optional fallback) — requires credentials at
~/.config/people-say/reddit.env. See skill.md Setup section. Useful if the
cookie path stops working or for headless use.
Both paths produce a `RedditClient` with a `.get(path, params)` method.
"""
import os
import sys
import time
from pathlib import Path
import requests
CRED_PATH = Path.home() / ".config" / "people-say" / "reddit.env"
TOKEN_CACHE = Path.home() / ".config" / "people-say" / ".reddit_token"
USER_AGENT = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 "
"(KHTML, like Gecko) Version/17.5 Safari/605.1.15"
)
OAUTH_USER_AGENT_TEMPLATE = "people-say:v2.0 (by /u/{username})"
OAUTH_BASE = "https://oauth.reddit.com"
WWW_BASE = "https://www.reddit.com"
class RedditClient:
def __init__(self, base, headers, cookies=None):
self.base = base
self.headers = headers
self.cookies = cookies
def get(self, path, params=None, timeout=30):
url = self.base + path
return requests.get(
url, headers=self.headers, cookies=self.cookies,
params=params, timeout=timeout,
)
def _try_browser_cookies():
try:
import browser_cookie3
except ImportError:
return None
for fn_name in ("brave", "safari", "chrome", "firefox"):
try:
fn = getattr(browser_cookie3, fn_name)
cj = fn(domain_name="reddit.com")
cookies = list(cj)
has_session = any(c.name == "reddit_session" for c in cookies)
if has_session:
return cj, fn_name
except Exception:
continue
return None
def _load_env_file(path):
out = {}
if not path.exists():
return out
for raw in path.read_text().splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
out[k.strip()] = v.strip().strip('"').strip("'")
return out
def _oauth_credentials():
file_vals = _load_env_file(CRED_PATH)
creds = {}
for key in ("REDDIT_CLIENT_ID", "REDDIT_CLIENT_SECRET",
"REDDIT_USERNAME", "REDDIT_PASSWORD"):
creds[key] = file_vals.get(key) or os.environ.get(key)
if all(creds.values()):
return creds
return None
def _read_cached_oauth_token():
if not TOKEN_CACHE.exists():
return None
try:
text = TOKEN_CACHE.read_text().strip().split("\n")
token, expires_at = text[0], float(text[1])
if expires_at > time.time() + 60:
return token
except Exception:
return None
return None
def _write_cached_oauth_token(token, expires_in):
TOKEN_CACHE.parent.mkdir(parents=True, exist_ok=True)
expires_at = time.time() + float(expires_in)
TOKEN_CACHE.write_text(f"{token}\n{expires_at}\n")
TOKEN_CACHE.chmod(0o600)
def _get_oauth_token(creds):
cached = _read_cached_oauth_token()
if cached:
return cached
ua = OAUTH_USER_AGENT_TEMPLATE.format(username=creds["REDDIT_USERNAME"])
auth = requests.auth.HTTPBasicAuth(creds["REDDIT_CLIENT_ID"],
creds["REDDIT_CLIENT_SECRET"])
data = {
"grant_type": "password",
"username": creds["REDDIT_USERNAME"],
"password": creds["REDDIT_PASSWORD"],
}
resp = requests.post(
"https://www.reddit.com/api/v1/access_token",
auth=auth, data=data, headers={"User-Agent": ua}, timeout=15,
)
if resp.status_code == 401:
print("ERROR: Reddit OAuth returned 401. Check credentials in "
f"{CRED_PATH}.", file=sys.stderr)
sys.exit(2)
resp.raise_for_status()
payload = resp.json()
token = payload.get("access_token")
if not token:
print(f"ERROR: No access_token in Reddit response: {payload}",
file=sys.stderr)
sys.exit(2)
_write_cached_oauth_token(token, payload.get("expires_in", 3600))
return token
def get_client():
"""Return a RedditClient using whichever auth path works."""
cookie_result = _try_browser_cookies()
if cookie_result:
cj, browser = cookie_result
return RedditClient(
base=WWW_BASE,
headers={"User-Agent": USER_AGENT, "Accept": "application/json"},
cookies=cj,
)
creds = _oauth_credentials()
if creds:
token = _get_oauth_token(creds)
ua = OAUTH_USER_AGENT_TEMPLATE.format(username=creds["REDDIT_USERNAME"])
return RedditClient(
base=OAUTH_BASE,
headers={"Authorization": f"bearer {token}", "User-Agent": ua},
)
print(
"ERROR: No Reddit auth available.\n"
"Tried: browser_cookie3 (no reddit_session cookie found in Brave/Safari) "
"and OAuth env file (missing).\n"
"Fix: either log into reddit.com in Brave or Safari, OR create "
f"{CRED_PATH} per the skill.md Setup section.",
file=sys.stderr,
)
sys.exit(2)
def url_to_api_path(url):
"""Convert a reddit thread URL to a path for either www.reddit.com or oauth.reddit.com."""
import re
from urllib.parse import urlparse
u = urlparse(url)
m = re.search(r"/r/([^/]+)/comments/([^/]+)", u.path)
if not m:
return None
return f"/r/{m.group(1)}/comments/{m.group(2)}"
What It Is
The problem it solves: searching "is X any good" gets you SEO-bait listicles, affiliate rankings, and review sites full of incentivized praise. The useful answers live where practitioners talk to each other, and that's all this skill reads. Recent real asks: /people-say BoldSign esignature, /people-say Creality K1C -- reliability, first printer experience.
It treats absence as data: if nobody anywhere talks about a product, that silence is a finding, not a gap to fill with a review aggregator's stars.
What It Needs to Run
- Claude Code on a Mac (the version of Claude that runs on your own computer rather than in a browser tab), with permission to reach the open web.
- Four small Python scripts, because the richest source is the hardest to reach: Reddit blocks automated access, so the skill's scripts sign in with the session cookies from Ida's own browser. This is why the skill can't run inside ChatGPT or the Claude website: those run on a company's servers and can't see your browser's logins. It needs your machine.
- Nothing else. No API keys to buy, no services to subscribe to.
How a Run Goes
How It Improves
The skill's last step is about itself. After a run, if something concrete went wrong, or the instructions got in the way of a good result, the AI proposes a specific edit to the skill file, checks it against a running log of past corrections it keeps in Ida's private notes (not part of this skill's files), and applies the change once Ida approves. Most runs end without firing this step; the file grows only when a run teaches it something.
Changes So Far
The changes that shaped it, dated where the records date them:
Old enough that a version of it was demoed in client trainings that spring; the walkthrough in one session's follow-up materials is this page's ancestor.
Reddit locked out unauthenticated tools, so the skill's richest source went dark. Access was rebuilt around the session cookies of Ida's own signed-in browser, and the Bluesky search tool was added in the same overhaul.
Ida's deep-background-research skill was rewritten to hand its what-do-real-people-say questions to People Say instead of running the same searches itself. Her museum-exhibitions skill later copied People Say's workaround for websites that turn away automated visitors.
Two checks went in just before the report step: the self-audit described above (real quotes, real people, honest counts), and a second gate that strips out verdicts the research wasn't asked to deliver. The skill file numbers its steps, and the patch is still visible there: the new checks squeeze in as 3.5 and 3.6.