x-keyword-comment — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited x-keyword-comment (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
keyword + reply intent → search X tweets → read tweet content → generate contextual replies → post to reply area
All process output to user (progress updates, process notifications) follows the user's language.
Search X by keyword, read each tweet's content, generate contextual replies based on a configured brand persona, and post them — all within a browser-act session.
config/keyword-comment-config.json has been filled in with actual product, persona, and tone values (all YOUR_* placeholders replaced before first run){SESSION} is a temporary, per-run session name used in all browser-act --session {SESSION} commands below. It is generated at execution start (e.g., xkc-{timestamp}) and not persisted across runs.
If browser-act has been confirmed available in the current conversation → skip this step.
Invoke browser-act via Skill tool to load usage. If installation or configuration issues arise, follow its guidance to resolve then retry.
python -c "
import json, pathlib, sys
for base in ['.claude/skills/x-keyword-comment', 'output/x-keyword-comment']:
cfg = pathlib.Path(base) / 'config/keyword-comment-config.json'
if cfg.exists():
print(json.dumps(json.loads(cfg.read_text(encoding='utf-8')), ensure_ascii=False, indent=2))
sys.exit(0)
print('ERROR: config/keyword-comment-config.json not found', file=sys.stderr)
sys.exit(1)
"Hold product.*, persona.*, tone.* fields in working memory for reply composition.
List available browsers:
browser-act browser listbrowser-act browser create --type stealth --headed), then repeat the list step.Once the user selects a browser, record its ID as {BROWSER_ID} for this run.
Generate a unique session name (e.g., xkc-{timestamp}) as {SESSION}. Open the browser:
browser-act --session {SESSION} browser open {BROWSER_ID} https://x.com/ --headedIf the browser is already open with an active session, list sessions and reuse:
browser-act session listPick the session associated with {BROWSER_ID} and assign its name to {SESSION}.
If X login status has been confirmed in the current conversation → skip this step.
Otherwise: browser-act --session {SESSION} get markdown and check:
@username, top navigation shows Home / Explore → logged in, continueUser refuses or cannot log in → terminate execution.
This Skill's operational boundary = what the user can manually do in their browser. It only reads data already displayed to the logged-in user, never bypassing authentication or access controls. JS code is encapsulated in Python files underscripts/, invoked viabrowser-act --session {SESSION} eval "$(python scripts/xxx.py {params})".$(...)is bash syntax; use the bash tool for execution.
Below are all atomic capabilities discovered and verified during the exploration phase, listed by command template with parameters. Simply invoke them as needed — no need to read scripts/*.py source code or re-verify. Only inspect scripts when execution fails for troubleshooting. Combine freely as needed during execution.
Warm up the account before posting replies to simulate organic browsing behavior.
Skip condition: warmup already performed today and less than 4 hours ago, or user says "fast mode".
Step 1 — Check notifications and messages (2–3 min)
browser-act --session {SESSION} navigate "https://x.com/notifications"
browser-act --session {SESSION} wait stable
browser-act --session {SESSION} get markdown
sleep $((RANDOM % 31 + 60)) # 60–90 s
browser-act --session {SESSION} navigate "https://x.com/messages"
browser-act --session {SESSION} wait stable
browser-act --session {SESSION} get markdown
sleep $((RANDOM % 31 + 30)) # 30–60 sStep 2 — Browse feed and like (3–5 min)
browser-act --session {SESSION} navigate "https://x.com/home"
browser-act --session {SESSION} wait stable
browser-act --session {SESSION} get markdownRandomly pick 3–5 tweets from the feed. For each:
browser-act --session {SESSION} navigate "{tweet URL}"
browser-act --session {SESSION} wait stable
sleep $((RANDOM % 26 + 15)) # 15–40 s
# If content is relevant → like it:
browser-act --session {SESSION} state
browser-act --session {SESSION} click {Heart index} # element with aria-label containing "Like"
browser-act --session {SESSION} wait stable
sleep $((RANDOM % 8 + 8)) # 8–15 s
browser-act --session {SESSION} navigate "https://x.com/home"
sleep $((RANDOM % 16 + 10)) # 10–25 sTarget: like 1–3 tweets; daily cap 20–30 likes (avoid fast bulk likes that trigger rate limits).
Step 3 — Keyword search browsing (2–3 min)
browser-act --session {SESSION} navigate "https://x.com/search?q={KEYWORD_ENCODED}&f=live"
browser-act --session {SESSION} wait stable
browser-act --session {SESSION} get markdownOpen 2–3 results, spend 25–60 s each reading the full tweet (as reply material).
Pre-action pause
sleep $((RANDOM % 61 + 60)) # 60–120 s — simulate "browse first, then reply"After navigating to the X search results page, scan all tweets with their reply button indices and content.
browser-act --session {SESSION} navigate "https://x.com/search?q={KEYWORD_ENCODED}&src=typed_query&f=live"{KEYWORD_ENCODED} is URL-encoded (spaces as %20)f=live returns newest tweets; omit for Top tweetsbrowser-act --session {SESSION} wait stable --timeout 30000browser-act --session {SESSION} scroll down --amount 1500 → browser-act --session {SESSION} wait stable --timeout 10000 → re-scanbrowser-act --session {SESSION} eval "$(python scripts/scan-search-tweets.py --limit {N})"Parameters:
--limit: max tweets to return, default 10Output example:
{
"totalReplyBtns": 8,
"tweets": [
{
"i": 0,
"tweetSnippet": "Breaking: Alibaba just killed the browser automation stack...",
"authorHandle": "@AIGuideHQ",
"authorUrl": "https://x.com/AIGuideHQ",
"tweetUrl": "https://x.com/AIGuideHQ/status/2051969984847286536",
"replyBtnIdx": 0
}
]
}`replyBtnIdx` note: This is the reply button's position index among all [data-testid="reply"] buttons currently on the page. After posting a reply, the DOM partially updates (new reply inserts), shifting subsequent indices — re-run `scan-search-tweets.py` after each reply to get fresh indices before the next one.Click the reply button for a specific tweet to open the reply input box.
browser-act --session {SESSION} eval "$(python scripts/click-reply.py {replyBtnIdx})"
Parameters:
{replyBtnIdx}: the tweet's reply button index (positional argument, from scan-search-tweets.py)Output example (success):
{
"ok": true,
"replyBtnFound": true,
"totalReplyBtns": 8
}Output example (out of range):
{
"ok": false,
"reason": "reply_btn_out_of_range",
"total": 8
}Architecture note: X uses the Draft.js editor (public-DraftEditor-content).document.execCommand('insertText')only updates the DOM without triggering React internal state — the submit button stays disabled. You must use browser-act's nativeinputcommand to simulate real keyboard input to activate the submit button. This is the only reliable method.
After clicking the reply button (click-reply.py), complete text input and submission:
browser-act --session {SESSION} wait --selector '[data-testid="tweetTextarea_0"]' --state attached --timeout 10000browser-act --session {SESSION} state → find aria-label=Post text role=textbox → note {EDITOR_IDX}browser-act --session {SESSION} input {EDITOR_IDX} '{reply_text}'browser-act --session {SESSION} state → find button labeled Reply → note {REPLY_BTN_IDX}browser-act --session {SESSION} click {REPLY_BTN_IDX}browser-act --session {SESSION} wait stable --timeout 10000Success signal: browser-act --session {SESSION} network requests --filter CreateTweet --method POST --status 200 returns at least 1 record.
Closing the editor: If the editor is empty, pressing Escape dismisses it directly with no dialog. If text has been typed and Escape is pressed (or the modal is otherwise closed), X shows a "Save post?" confirmation dialog (Save / Discard). To discard:browser-act --session {SESSION} state→ findDiscardbutton index →browser-act --session {SESSION} click {DISCARD_IDX}
All operations remain on the X search page — no navigation to individual tweet detail pages required.
Config: Load config/keyword-comment-config.json and hold product.*, persona.*, tone.* fields in working memory before proceeding:
python -c "
import json, pathlib, sys
for base in ['.claude/skills/x-keyword-comment', 'output/x-keyword-comment']:
cfg = pathlib.Path(base) / 'config/keyword-comment-config.json'
if cfg.exists():
print(json.dumps(json.loads(cfg.read_text(encoding='utf-8')), ensure_ascii=False, indent=2))
sys.exit(0)
print('ERROR: config/keyword-comment-config.json not found', file=sys.stderr)
sys.exit(1)
"browser-act --session {SESSION} navigate "https://x.com/search?q={KEYWORD_ENCODED}&src=typed_query&f=live" → browser-act --session {SESSION} wait stable --timeout 30000browser-act --session {SESSION} eval "$(python scripts/scan-search-tweets.py --limit {N})" → candidate tweet listbrowser-act --session {SESSION} scroll down --amount 1500 → browser-act --session {SESSION} wait stable --timeout 10000 → re-scan, merge resultsauthorUrl / tweetSnippet (skip promotional or low-relevance tweets)intent (caller-provided) + tweetSnippet + authorHandle + loaded config (product.*, persona.*, tone.*) to compose a 60–180 character ASCII reply. See references/quality-checklist.md (7-item checklist) and references/reply-composition.md (3 recommendation scenarios A/B/C).browser-act --session {SESSION} eval "$(python scripts/click-reply.py {replyBtnIdx})" → open reply boxbrowser-act --session {SESSION} wait --selector '[data-testid="tweetTextarea_0"]' --state attached --timeout 10000browser-act --session {SESSION} state → get editor index {EDITOR_IDX} → browser-act --session {SESSION} input {EDITOR_IDX} '{reply_text}'browser-act --session {SESSION} state → get Reply button index {REPLY_BTN_IDX} → browser-act --session {SESSION} click {REPLY_BTN_IDX}browser-act --session {SESSION} wait stable --timeout 10000browser-act --session {SESSION} network requests --filter CreateTweet --method POST --status 200sleep $((60 + RANDOM % 120)) (60–180 s between replies)scan-search-tweets.py to refresh replyBtnIdx values before the next replyOutput per tweet:
{
"authorUrl": "https://x.com/AIGuideHQ",
"tweetUrl": "https://x.com/AIGuideHQ/status/2051969984847286536",
"tweetSnippet": "Breaking: Alibaba just killed the browser automation stack...",
"replyText": "The captcha point is real -- Playwright + Cloudflare means more glue than logic...",
"posted": true,
"skippedReason": null
}DOM Pagination: Search results load as an infinite scroll. Trigger more: browser-act --session {SESSION} scroll down --amount 1500 → browser-act --session {SESSION} wait stable --timeout 10000 → re-scan. Termination: totalReplyBtns does not increase across 2 consecutive scrolls, or target reply count is reached.
posted == true for each tweet, confirmed by browser-act --session {SESSION} network requests --filter CreateTweet --method POST --status 200 returning at least 1 record (editor disappearing after submission is a secondary signal only).
browser-act input {idx} '{text}' on Windows cmd (GBK active codepage) will corrupt non-ASCII characters (em-dash, full-width quotes, emoji) passed as arguments. Scripts call sys.stdout.reconfigure(encoding='utf-8', newline='\n'). Callers must also ensure UTF-8 terminal: run chcp 65001 or set PYTHONUTF8=1, or restrict reply text to ASCII-only charactersdocument.execCommand('insertText') only updates the DOM without triggering React state — submit button stays disabled. Must use browser-act native input commandreplyBtnIdx changes with DOM updates; must re-run scan-search-tweets.py after every replyposted status + tweetUrl + tweetSnippet hash) incrementally; on failure, resume from breakpointPath: {working-directory}/browser-act-skill-forge-memories/x-keyword-comment-x-keyword-comment.memory.md (working directory is determined by the Agent running the Skill, typically the project root or current working directory)
Before execution: If the file exists, read it first — it records unexpected situations encountered during past executions (e.g., a strategy has become ineffective); adjust strategy order accordingly.
After execution: If an unexpected situation is encountered (strategy became ineffective, page redesigned, anti-scraping upgraded, better path discovered), append a line: {YYYY-MM-DD}: {what happened} → {conclusion}
Normal execution does not write to the file. Do not record what keywords were used or how many results were returned — those are task outputs, not experience.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.