<img src="site/icon-64.png" alt="x-agent-skill mark" width="64" height="64">
# The X agent skill
Site: https://muhammadhassan03.github.io/x-agent-skill/
By [@mirza_hassan07](https://x.com/mirza_hassan07) on X
Twelve skills and ten offline tools that draft, lint and humanize what you post on X.
Nothing here posts, scrapes or predicts reach.
Free, MIT, no signup, no API key, nothing to connect.

*A rendering of real tool output (`postscore.py`, `thread.py`, `humanize.py`, `detect.py` run on invented sample drafts), drawn by a script. It is not a terminal recording: VHS did not run ([how it was made](demo/README.md)).*
**The skills write. You post.** There is no X API client, no browser automation,
no scraping, no extension and no "paste this in DevTools" script anywhere in this
repo. X's Automation Rules list "non-API-based forms of automation, such as
scripting the X website" under Don't and say it may lead to permanent suspension.
X's page is aimed at developers and defines automation as repeated actions taken
without a person actively performing them. This repo takes the cautious reading: a
human posts everything, and nothing here touches X at all.
The ten tools the skills use are Python standard library only, make no network calls and upload
nothing. They run on your machine, on your text. The one exception is `tools/algo_watch.py`, a
maintainer script run by CI that fetches a single public file from GitHub; it is not part of the plugin.
## Why this one
This pack is built around three things:
1. **Designed not to act on X.** Nothing here posts, likes, follows, DMs or scrapes.
`/x-viral` works from posts you note down by hand. `/x-dm` drafts one message at a
time for you to send. That is a design choice to stay clear of X's automation rules,
not a guarantee about how X will read your account. See `references/x-rules.md`.
2. **Measured, and the scorer failed.** The post scorer and the outlier ranker were
tested on a public corpus. The scorer does not predict likes (AUC 0.50). The full
result is below.
3. **Tools that run.** Ten dependency-free tools, a test suite that runs on every push, CI on Python 3.9, 3.11
and 3.13 on Linux and Windows. A repo test fails the build on any em dash, any
network or browser import in a tool, and any skill that lacks the approval gate.
Default persona: a small free account (280 weighted characters, link in the first
reply, reply-first growth) earning off-platform through clients, products or a
newsletter. Premium features are documented and labelled **documented, not tested
by the author**.
## Install
You need Python 3.9 or newer on your PATH for the tools (nothing to `pip install`). Check with
`python --version` (Windows) or `python3 --version` (macOS, Linux).
As a plugin, inside Claude Code:
```
/plugin marketplace add MuhammadHassan03/x-agent-skill
/plugin install x-agent@x-agent-skill
```
Plugin skills are namespaced, so the commands are `/x-agent:x-post`, `/x-agent:x-thread` and so on.
Wherever this page says `/x-post`, that is what you type. Ask Claude "use the x-post skill" and it
finds the same skill.
Or by hand. macOS and Linux:
```bash
git clone https://github.com/MuhammadHassan03/x-agent-skill.git
mkdir -p ~/.claude/skills
cp -r x-agent-skill/skills/x-* ~/.claude/skills/
```
Windows, PowerShell (`cp -r` does not work there, and no git? download the ZIP from the green
Code button and unzip it instead):
```powershell
git clone https://github.com/MuhammadHassan03/x-agent-skill.git
New-Item -ItemType Directory -Force "$HOME\.claude\skills" | Out-Null
Copy-Item -Recurse x-agent-skill\skills\x-* "$HOME\.claude\skills\"
```
Or with no Claude Code at all: paste one `SKILL.md` at the top of a chat and it runs as a
mode. You lose the Python tools, which is most of the point of `/x-post`, `/x-reply` and
`/x-human`, but the writing rules still apply.
On Windows, where a skill says `python3`, use `python`. A stock Windows install answers `python3`
with "Python was not found; run without arguments to install from the Microsoft Store" and exits
non-zero; that is the Store shortcut, not a broken install. If `python` is missing too, try `py`.
Keep the `skills/` folders side by side: `x-thread`, `x-reply`, `x-viral` and `x-article` import or
read files from `x-post`. The `references/` and `templates/` folders are not copied by the by-hand
install, so open them in the clone (the skills tell Claude what is in them where it matters).
Then copy `templates/voice.md` to `~/.claude/x/voice.md` (`$HOME\.claude\x\voice.md` on Windows,
create the folder first) and fill it in, or send Claude three of your own posts and say "write my
voice.md from these". For several accounts see `templates/accounts.md`.
### First run, 60 seconds, no Claude needed
From the clone, run the scorer on the sample drafts. This proves Python and the tools work before
you involve any skill:
```bash
cd x-agent-skill
python3 skills/x-post/postscore.py demo/samples/good.txt # Windows: python
python3 skills/x-post/postscore.py demo/samples/bait.txt # the same tool blocking a bad draft
```
`good.txt` scores 93.8 and passes; `bait.txt` is blocked for engagement bait and exits non-zero. Your
own text works the same way: save it to a file (any encoding PowerShell writes is fine) or pipe it
in with `-` as the file name. Then open Claude Code in any folder and say "write a post about ...".
For a full walk through one post, from install to the FIRST REPLY block, with real tool output, read
the [/x-post tutorial](docs/tutorial-x-post.md).
## The twelve
| command | what it does | tool |
| --- | --- | --- |
| `/x-post` | One idea into a post. Three hooks from [19 formulas](skills/x-post/hooks.json), linted, one humanized draft, counted the way X counts. | `postscore.py`, `xcount.py` |
| `/x-thread` | A long draft into a thread, 280 weighted characters per post, links gathered in the last post. | `thread.py` |
| `/x-reply` | Replies under other people's posts, checked against an approximation of the 0-3 reply rubric. Refuses compliment-only openers and self-reply chains. | `replylint.py` |
| `/x-viral` | Ranks posts you collected by hand by multiple over each account's own median. Never browses X. | `swipe.py` |
| `/x-article` | Long-form outline, draft, first-screen hook, originality checklist. Premium, not tested by the author. | `xcount.py` |
| `/x-human` | The humanizer and a five-check panel, with an X lexicon block. | `humanize.py`, `detect.py` |
| `/x-profile` | Bio, pinned post, banner, link, scored on a 100-point [rubric](skills/x-profile/rubric.json). | |
| `/x-plan` | The week: what to post, which format, when, 10 accounts to reply to. | |
| `/x-dm` | One first message and two follow-ups for a specific person. Then stop. | |
| `/x-audit` | Reads your own analytics CSV export and ranks your posts by outlier multiple. | `xanalytics.py` |
| `/x-money` | Original Content Rewards eligibility tracker and ineligible-content linter. Refuses earnings estimates without your own rate. | `ocr.py` |
| `/x-algo` | Regenerates the pinned ranking-weights reference from a `param.rs` you paste. | `weights.py` |
Every skill ends in the same gate: the finished text in a copy-ready block, then
"Reply yes to log it". A "yes" appends one line to a log file on your disk. It never posts.
## The tools, with real output
**Counting.** X counts weighted characters: most characters weigh 1, CJK and most
emoji weigh 2, every URL weighs 23. `xcount.py` vendors the public twitter-text
config and handles emoji sequences.
```
76 / 280 weighted (free) FITS
remaining 204, urls 1 (each 23), weight-2 characters 0
```
**Scoring a post.** Five checks: hook, specificity, reply pull, link, bait. A link in the root post of a
free-account draft is a hard stop: the score is capped at 49 and the draft is blocked, with the fix
(`--split-link` prints the POST and FIRST REPLY blocks).
```
POST SCORE 31.8 WEAK (161/280 weighted)
score capped at 49: link in root post
LINK .................... 0.0 1 link(s) in root post
BAIT .................... 0.0 like/repost solicitation, "RT/like if" bait, "follow for more"
BLOCKING: Link in root post. On free accounts, links in the root post were observed to cost reach (observed, not coded). Put it in the first reply.; engagement bait; hashtag wall (more than 2)
```
Anything that needs a number you did not give is written `{{your number}}`, and
the scorer refuses to pass a draft that still contains one.
**Replies.**
```
REPLY BAND 0/3 SPAM-LIKE (12 weighted) <- "Great point!"
FAIL no generic praise reads as generic praise or agreement with nothing added
FAIL on topic shares no content words with the parent post
```
**The humanizer.** Strips invisible Unicode, dashes, stock vocabulary and X
phrases like "let that sink in", flags structural tells for you to rewrite, then scores.
```
17.8 FLAGGED -> 50.1 REVIEW (+32.3)
```
That is one mechanical pass on a sample draft: better, still not a pass. The structural tells it
flags (numbered-emoji bullets, hashtag walls, "not just X, it's Y") are left for you to rewrite.
X posts are short, so a check that needs more text than the draft has returns "too
short" and is left out of the verdict instead of scoring a made-up 50.
**Outlier ranking.** Raw views are not evidence; the multiple over the account's own
median is.
```
- **OUTLIER** 35.65x @alpha_demo (41,000 vs 1,150 computed) [Contrarian Take]
- **OUTLIER** 7.38x @beta_demo (2,400 vs 325 computed) [Price Reveal]
```
## What I actually measured, which is the part worth reading
Method: `measurement/validate.py` on the public
`enryu43/twitter100m_tweets` corpus joined to `enryu43/twitter100m_users`. Three of
its 41 shards, 6.4M tweet rows, tweets from 2019-01-01 to 2022-12-31, originals
only, accounts with at least 50 originals and a median of at least 5 likes. 1,500
accounts, 270,880 tweets scored. Every comparison is **within one account**, which
removes account size. A "hit" is a tweet with 3x or more its account's median likes
(37,861 of them); a "miss" is under 1x (131,549). The corpus has no licence and
X's Developer Policy forbids redistributing content, so I ran it locally and the
repo holds [aggregates only](measurement/results.json): no tweet text, IDs or handles.
**Does the scorer predict hits? No.** Within-account AUC, hits vs misses, macro-averaged
over 1,402 accounts with 95% bootstrap intervals:
| predictor | AUC |
| --- | --- |
| hook score | 0.500 (0.494 to 0.506) |
| full `postscore.py` total, link cap off (`--allow-link`) | 0.475 (0.469 to 0.482) |
| full `postscore.py` total, link cap on (default) | 0.449 (0.442 to 0.456) |
| baseline: tweet length alone | 0.592 (0.584 to 0.600) |
| baseline: number in first line | 0.527 (0.521 to 0.532) |
0.50 is a coin flip. The score does not forecast likes, and a trivial length feature
beats it. I did not tune it on this data afterwards, because that would just fit the
test set. So `postscore.py` is a lint for the problems it names (unfilled
placeholders, bait, thread openers, links in the body, being over length), not a
prediction, and the SKILL.md says so.
The link hard stop lowers the full-score AUC from 0.475 to 0.449: in this corpus posts with links got *more*
likes, so capping them points the wrong way here. The corpus predates the ranker and the link suppression, so the link cap is a policy choice, not a finding. The cap rests on a 2025 outside observation,
and `--allow-link` turns it back into a warning.
**Rule effects, within account** (mean difference in log2 of the multiple; positive means
more likes than that account's norm):
| rule | effect | accounts |
| --- | --- | --- |
| link in the post | +0.59 (+0.53 to +0.65) | 1,398 |
| hashtag wall (3+) | +0.40 (+0.32 to +0.47) | 627 |
| number in the first line | +0.16 (+0.13 to +0.20) | 1,493 |
| question | +0.01 (-0.04 to +0.06) | 887 |
| thread opener | +0.33 (-0.10 to +0.76) | 19, too few |
| engagement bait | +0.73 (-1.00 to +2.33) | 7, too few |
Read this carefully. In 2019-2022 data, **posts with links and hashtag walls got more
likes than the same account's other posts**, the opposite of what the scorer penalises.
That does not make the scorer's link penalty wrong for X today: the corpus ends in April
2023, before Premium reach changes, before the open-sourced ranking code and before
Original Content Rewards; likes are not reach; and links in that era were often
news or brand posts. It does mean the link advice rests on a 2025 outside observation
(Buffer, 18.8M posts), which is labelled "observed, not coded", and not on anything I measured.
Bait and thread openers were too rare in this corpus to say anything.
**What the outlier multiple shows, and what it does not.** Dividing by an account's own median removes account size, and the numbers below show it does. They do not show that the multiple finds good posts.
| | Spearman with follower count |
| --- | --- |
| raw likes | 0.566 |
| outlier multiple | 0.025 |
Raw likes mostly measure account size. The multiple removes that.
**How many posts is "enough" for a baseline?** The tools require 3. That is a floor, not
accuracy. Median absolute error of a baseline built from k random posts, in log2 units
(0.59 means off by about 1.5x):
| k posts | median error | 90th percentile |
| --- | --- | --- |
| 3 | 0.59 | 1.81 (about 3.5x) |
| 5 | 0.49 | 1.38 |
| 10 | 0.32 | 0.97 |
| 20 | 0.23 | 0.68 |
So a 3-post multiple of 3x can be an ordinary post. Capture 10 per account if you can, or
supply the account's typical views yourself.
## 30-day dogfood: in progress, numbers land on 2026-11-19
One free account (@mirza_hassan07), 2026-10-21 to 2026-11-19: one post a day, 5 to 10 replies,
link in the first reply, a real hand edit on every draft, goal is client work. The routine is in
[DOGFOOD.md](DOGFOOD.md). On day 30 the author exports their own analytics CSV and runs
[`measurement/dogfood/report.py`](measurement/dogfood/report.py), which prints aggregates only
(no post text) and goes here unedited, good or bad. One account over one month shows nothing
about cause, and the section will say so. Until then there are no dogfood numbers, and none are
implied anywhere above.
## Algorithm changelog
X publishes its ranking parameters and they move. A weekly workflow runs `tools/algo_watch.py`, which
compares `param.rs` with [references/weights.json](references/weights.json). When anything changed it
runs the tests and commits the update straight to the default branch, with the diff in the commit
message (including the upstream commit hash and date). Before committing it validates the parse:
every weight key from the previous snapshot must still be present, every weight value must be
numeric, and no single weight may move by more than 10x in one run (going to or from zero, or
flipping sign, counts). If any check fails, the file moves or anything unexpected happens, it commits
nothing and opens an issue with the raw diff. Fetching, parsing and testing run in a read-only job;
only a separate job can push, and it first checks the staged files against an allowlist (the trust
model is in [SECURITY.md](SECURITY.md)). The last three entries, generated by that script:
<!-- algo-changelog:start -->
#### 2026-10-24, 7 changes (commit 6258a15)
Source commit date: 2026-10-24T03:22:45Z. Source: https://github.com/xai-org/x-algorithm/blob/main/home-mixer/params/param.rs
0 of the changes are ranking weights; the rest are other parameters and feature defaults.
| change | param | before | after |
|---|---|---|---|
| added (param) | `EnableAdsFunnelTrace` | | true |
| changed (param) | `EnableFairShareRanking` | false | true |
| added (param) | `EnableNewUserSoftNsfwFilter` | | false |
| added (param) | `EnableRecentSearchQueriesHydration` | | false |
| changed (param) | `EnableVideoCarouselCandidatesLogging` | false | true |
| added (param) | `NewUserSoftNsfwFilterMaxAccountAgeSecs` | | 86400 |
| changed (param) | `ShadowTrafficPhoenixClusterRates` | `vec![]` | `vec!["Experiment18Fou:0.3".to_string()]` |
#### 2026-10-21, 3 changes (commit 7f2ca2b)
Source commit date: 2026-10-21T00:52:08Z. Source: https://github.com/xai-org/x-algorithm/blob/main/home-mixer/params/param.rs
0 of the changes are ranking weights; the rest are other parameters and feature defaults.
| change | param | before | after |
|---|---|---|---|
| added (param) | `EnableFairShareRanking` | | false |
| added (param) | `EnableVideoCarouselCandidatesLogging` | | false |
| added (param) | `RetrievalValueModelWeights` | | |
#### 2026-10-21, baseline (195 params, commit 74d49a4)
Source commit date: 2026-10-20T04:32:20Z. Source: https://github.com/xai-org/x-algorithm/blob/main/home-mixer/params/param.rs
First snapshot. Later entries list what changed against it.
Full history: [references/ALGO-CHANGELOG.md](references/ALGO-CHANGELOG.md).
<!-- algo-changelog:end -->
## The fine print, which is the honest part
- **It does not post, like, follow, reply, DM or scroll, and never will.** A pull request
that adds any of that is closed.
- **It cannot know how a post will perform.** The measurement above says so.
- **Nothing is undetectable.** `/x-human` runs five local heuristics. They are not GPTZero,
Originality, Copyleaks, Winston or Turnitin, and call none of them. Reply downvotes on X can
list "AI generated" as a reason (reported, Premium users, March 2026, unverified here), and
posts "created or posted using automated means" are ineligible for Original Content Rewards.
Read and edit every post.
- **The reply rubric is not published.** The open-sourced code scores replies 0 to 3 with a
Grok model when the parent or root has over 250,000 followers, but the prompt is excluded
"to reduce gameability". `replylint.py` is an approximation of it. Smaller accounts go
through a different spam classifier.
- **No coded link penalty was found** in the ranking parameters. Link advice is "observed, not coded".
- **Weights scale predicted probabilities, not counts.** The file's own comments call "one
report cancels 468 likes" wrong. This repo never says it. The weights in this repo are the
ones in the file at the pinned commit (like 0.66, click 0.3, dwell 0.05, not-interested
-52.272), not the older figures many guides quote.
- **No earnings estimates.** X publishes no per-impression rate. `/x-money` refuses to
estimate unless you supply your own rate, and labels the result as your assumption.
- **Premium features are untested.** Articles, 25,000-character posts, Original Content
Rewards and Subscriptions are documented from X's pages, last checked 2026-10-21. The
Subscriptions gate (2,000 verified followers, 5M impressions, about 97% share) is
**unverified**; check the app.
- **Tracker commits are made by a workflow, not by a person.** They are authored as
"x-agent-skill algo-watch" (GitHub Actions' noreply address), validated by `--verify-tree` before
they are copied in, and only ever touch `references/` and the changelog block in this README. They
never touch `skills/` or `tools/`.
- **Numbers go stale.** Every one is in [references/stale.md](references/stale.md) with its
date and source. The weights table is regenerated by `/x-algo` and pinned to a commit.
- **Tested** locally on Python 3.15 and 3.9.25 on Windows, and in CI on 3.9, 3.11 and 3.13 on
both ubuntu and windows. The scorer and tools were not tested on macOS.
## Files
```
.claude-plugin/ plugin.json, marketplace.json
skills/x-*/ SKILL.md plus the tools beside it
templates/ voice.md, accounts.md
references/ x-algorithm.md (pinned), x-rules.md, stale.md, weights.json, ALGO-CHANGELOG.md
tools/ algo_watch.py: the maintainer-only weekly tracker (not part of the plugin)
.github/workflows/ test (six jobs), pages (the site), algo-watch (three scoped jobs)
demo/ the README GIF, its tape and render scripts
measurement/ validate.py (needs pyarrow, numpy), results.json (aggregates)
measurement/dogfood/ report.py: day-30 numbers from your own analytics CSV
DOGFOOD.md the 30-day routine
site/ the landing page (static, no build step)
tests/ one test file per tool, invented fixtures
```
## License
MIT. Copyright (c) 2026 Muhammad Hassan.