optimize-linkedin
Skill4 starsChanged 9 days agoMITView on GitHub
Read the candidate's LinkedIn profile through the session browser, diff it against their targeting + evidence, and propose honest, evidence-backed rewrites of the headline / About / experience / skills / Featured, then write the approved edits back confirm-first per field. Dry-run preview is the default and always the first move. User-initiated, local-only. Capabilities = profile_optimize (read+suggest), profile_apply (write-back); platform = linkedin.
In this repository31 other files it gives its agents.
- AGENTS.mdAGENTS.md
- templates/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- templates/CLAUDE.mdCLAUDE.md
- answer-questionSkill
- apply-jobSkill
- calendar-syncSkill
- coach-gapsSkill
- company-healthSkill
- configureSkill
- discover-companiesSkill
- email-commsSkill
- evaluate-jobSkill
- ingest-mailSkill
- ingest-messagesSkill
- ingest-profileSkill
- intake-extractSkill
- interview-prepSkill
- reevaluate-strategySkill
- relationship-sourcingSkill
- report-issueSkill
- research-boardsSkill
- research-companySkill
- research-compSkill
- resume-extractSkill
- schedule-meetingSkill
- search-jobsSkill
- setup-searchesSkill
- sync-statusSkill
- tailor-applicationSkill
- track-outcomesSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
18 rules. About 0 are common, 6 are distinctive, and 12 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈6
Distinctive · estimate
This author's own choices. Worth a look.
12
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 62
Read + suggest: careerrat automation enable profile_optimize --write then careerrat automation enable profile_optimize linkedin --write
- Very specific: line 87
If candidate/ is not set up, run ingest-profile first — there is no truth bank to diff against otherwise.
- Distinctive: line 114
Read and record the current text, per surface, so the diff is exact:
- Very specific: line 139
Trace every line to evidence: No proposed bullet, metric, or claim without a backing evidence.yml claim.
- Very specific: line 174
Voice + length: Keep the About short and in the candidate's register — no corporate wall-of-bullets.
- Very specific: line 177
No placeholder brackets: If a detail is unknown, go generic or omit it — never emit [Company], [Role], [Name], or any bracket token (the …
- Very specific: line 180
Write the full set to a document the candidate can act on, in the loose _linkedin/ bucket (per the Artifact Contract — a profile doc isn't …
- Very specific: line 207
Only with profile_apply allowed, and only after the user approves specific fields.
- Very specific: line 212
If the in-app pass already staged fields, read the batch first (careerrat data linkedin-proposals latest --status reviewed, or the default …
- Distinctive: line 250
Never edit anything other than the candidate's own profile.
- Very specific: line 260
If the field was not already approved in the app, record the user's live yes first (careerrat data linkedin-proposals approve --batch <id> …
- Distinctive: line 271
Only mark a field applied after the reload-and-re-read confirms it — never on the write attempt alone.
- Distinctive: line 274
Every decision bumps the batch version, and the command above prints the updated batch in its response.
- Distinctive: line 289
If the pass surfaces nothing the candidate should act on, say so — don't manufacture changes.
- Very specific: line 296
On by default, two separate switches: Run a browser pass only where careerrat automation status --json shows the capability allowed: true …
- Distinctive: line 303
Dry-run preview is the default and the first move: Always produce the full read-only before→after set before writing anything.
- Very specific: line 311
Honesty firewall (hard): Every proposed or written line traces to an evidence.yml claim.
- Very specific: line 355
Domain-neutral: No hardcoded companies, roles, titles, codenames, or candidate-specific values in this skill.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
optimize-linkedin
A LinkedIn profile pass driven through the session browser. It reads the candidate's
live profile, diffs it against their own truth bank (targeting.yml + evidence.yml),
and proposes honest, evidence-backed rewrites so the profile reads for the roles they
actually want. It can then write the approved edits back, one confirmed field at a time.
Two separately-switched capabilities on the linkedin platform, both on by default:
profile_optimize(read + suggest) — reads the profile and produces a full before→after diff. Read-only. Never touches the live profile.profile_apply(write the approved edits) — applies the approved rewrites in the live profile through the session browser, confirm-first PER FIELD.
Reading/suggesting and writing carry different intent, so they have independent
switches checked through the same mayRun(); the write-back switch never changes
what the per-field confirmation in STEP 5 requires. Both halt on captcha / 2FA / login-wall.
Runs under AGENTS.md. These contracts bind without being restated here: Privacy Invariant (
current_basenever outbound), Honesty Firewall, Placeholder/Bracket Ban, Gate Write-back, Domain-Neutral Rule, Browser Automation Contract, Activity Pulse logging, Tracker verify+snapshot, and Sent-Clears-Draft. Inline reminders at point-of-use are intentional; standalone restatements point back to the relevant AGENTS.md section. Barecandidate/,workspace/,config/, and.internal/paths below are symbolic; resolve them per AGENTS.md's Path Resolution rule.
Dry-run preview is the default and always the first move. The skill always produces
the full read-only before→after set first — everything it would flag and change, nothing
touched — so the candidate sees the whole picture before a single field is written.
Applying is a deliberate second step, confirmed per field. This is the same dry-run-by-default
posture as apply-job's authenticated_apply_preparation: preview the diff, then write. It also means the
skill runs as a pure read-only audit even when profile_apply is off.
STEP 0: Capability check
Editing a public profile is modifying public-facing content, so STEP 5 keeps its per-field confirmation. The capability check below only says whether each part is on.
Run:
careerrat automation status --jsonInspect capabilities.profile_optimize and capabilities.profile_apply for platform
linkedin. For each, allowed: true means neither the capability's switch nor
linkedin's switch is turned off. This is the verdict from mayRun() in
src/core/automation/consent.mjs; never re-derive it in prose. Both are on by default.
Resolve the run mode from the two verdicts:
profile_optimizeallowed → run the read + suggest pass (STEPS 1–4). This is the whole skill for most users.profile_applyALSO allowed → after the user approves specific fields in STEP 4, you may write those fields back (STEP 5, confirmed per field).profile_applyallowed butprofile_optimizenot is not a valid run, you must read and propose before you can apply.Neither allowed → you can still run the lean / no-browser fix-doc path (see below), which drives no logged-in session. For the browser-driven pass, say it is turned off (if
readErroris set, say the settings file can't be read and name it), give the commands, and skip it:- Read + suggest:
careerrat automation enable profile_optimize --writethencareerrat automation enable profile_optimize linkedin --write - Write-back:
careerrat automation enable profile_apply --writethencareerrat automation enable profile_apply linkedin --write
- Read + suggest:
This skill is always user-initiated. Never run it unprompted or on a schedule.
STEP 1 — LOAD THE CANDIDATE TRUTH BANK
The profile is rewritten from the candidate's own data, never invented. Read:
candidate/profile.yml—domain,toolchain,location.*, and the outbound-safe comp fields.compensation.current_baseis private — it must never appear in any suggested or written profile copy.candidate/targeting.yml—role_buckets/ target titles,keep_signals,cut_signals. This is what the profile should read for — the target side.candidate/evidence.yml— the claim bank. Every proposed line must trace to a claim here (runcareerrat evidence listif useful). No claim → no line. This is the same honesty firewalltailor-applicationenforces.candidate/honesty.yml—educationpolicy,tools.confirmed/tools.do_not_claim,claims.do_not_fabricate. Overclaim caps apply to profile copy exactly as they do to a résumé.candidate/writing-style.md(+workspace/writing-samples/) — voice/register, so the rewrite sounds like the candidate, not like corporate filler.
If candidate/ is not set up, run ingest-profile first — there is no truth bank to
diff against otherwise.
STEP 2 — OPEN THE PROFILE IN THE SESSION BROWSER (read-only)
Navigate to the candidate's own LinkedIn profile in the session browser. The session
browser is Layer 3 per docs/BROWSER.md. Keep the provider on auto unless the user
explicitly changes it; CareerRat resolves the current session browser and reuses its
signed-in session.
Snapshot or read the current page before reading anything. Drive the live DOM
turn-by-turn; never rely on hardcoded selectors — the same model as apply-job and
ingest-messages.
On a login wall, captcha, 2FA prompt, or any unexpected interstitial, halt immediately and ask the user to complete it. Never bypass or automate around an auth challenge.
Save any screenshot or scraped fragment under workspace/ only.
Lean / no-browser fix-doc path. If no session browser is configured, the user wants suggest-only, or the capability is turned off, skip the live session entirely. Work from a read-only source instead — a Layer-1
WebFetchof the public profile, or a profile the user pastes in, and produce the same before→after document (STEP 4). No browser needed, no write-back. This is the default tier and a valid complete run on its own.
STEP 3 — READ THE CURRENT PROFILE SURFACES (capture verbatim)
Read and record the current text, per surface, so the diff is exact:
- Headline — the current line under the name.
- About — the full summary text.
- Experience — each role's title, employer, dates, and description body.
- Skills — the current skill list (note the 50-skill cap and which are pinned/top).
- Featured — the current Featured items (cards/links) and their titles/descriptions.
- Licenses & certifications — each credential, its issuer, and issue/expiry dates.
- Open-to-work / job preferences — the target-titles field (visible only to the user).
- Contact info — websites and their labels/types.
Capture each verbatim. Don't propose anything yet — STEP 4 owns the diff.
STEP 4 — DIFF + PROPOSE (the dry-run preview — always produced first)
For each surface, write the candidate's current text beside an honest, evidence-backed rewrite. Apply these rules — they are the crux of the skill:
Honest-vs-target field rule (load-bearing). Headline / About / Experience describe what the candidate ACTUALLY is and has done — strictly honest, no aspirational titles. Target titles the candidate is seeking (e.g. a role they want but haven't held) belong in the Open-to-work / job-preferences field, never the headline. The mistake to avoid: a target title in the headline and then absent from open-to-work. This is the honesty gate applied to the profile surface.
Trace every line to evidence. No proposed bullet, metric, or claim without a backing
evidence.yml claim. Honesty caps from honesty.yml apply (nothing in do_not_claim /
do_not_fabricate; education policy honored). Profile copy is held to the same standard
as a tailored résumé.
A flagged claim may be a stale evidence bank, not an overclaim. When a claim already on
the profile does NOT trace to evidence.yml, do not default to softening or cutting it — the
evidence bank may simply be out of date (a repo gained stars, a feature, or more pulls since
the claim was last written). Verify the specific against the source first: the candidate's own
repo (~/code/<repo>, gh repo view <owner>/<repo>) or their public portfolio. If it
verifies, the fix is to update evidence.yml — record the grounded fact plus a dated
source note — so the honest copy is backed, NOT to weaken it. Only if it cannot be
substantiated do you soften or cut. Confirm with the candidate either way. (The firewall flags
for human review; it does not auto-delete honest work.)
Expired-but-earned credentials are factual. A lapsed license or certification is still something the candidate genuinely earned — listing it is honest. The only dishonest move is hiding or forward-dating the expiry to imply current validity. Surface an expired credential as the candidate's choice — keep it shown-as-expired (transparent), recertify, or remove — never silently make it read as current.
Obfuscation / NDA convention. Internal employer project names are typically NDA and
must not be made public. The source of truth for what is already public is the
candidate's own public portfolio / OSS surface — repos and products they ship under
their own name keep their real names and real public stats (stars, listings,
downloads). On LinkedIn the employer is usually named, so describe internal employer work
generically (no internal codenames, no project names) with sensitive specifics
redacted. Keep only safe, already-public metrics; drop dollar figures, exact internal
headcounts/record counts, vendor/system lists, and internal tool/channel counts. The
current_base privacy invariant holds on everything.
Don't "fix" intentional styling. A candidate's deliberate brand styling in the headline (unusual separators, lowercase, symbols that match their portfolio) is a choice, not broken markup. Never flag stylistic choices as bugs.
Voice + length. Keep the About short and in the candidate's register — no corporate
wall-of-bullets. Match writing-style.md.
No placeholder brackets. If a detail is unknown, go generic or omit it — never emit
[Company], [Role], [Name], or any bracket token (the repo lint rejects them).
Write the full set to a document the candidate can act on, in the loose _linkedin/
bucket (per the Artifact Contract — a profile doc isn't a per-company round), e.g.
~/Documents/careerrat/_linkedin/linkedin-optimize-<yyyy-mm-dd>.md, formatted current → proposed per
surface, with a one-line rationale and the backing evidence claim id for each change.
Present the same diff inline to the user.
Record the batch for in-app review (same pass, after the doc): persist the same per-surface diff as a pending proposal batch so the Ask workspace can render it for per-field approval:
careerrat data linkedin-proposals record --data '<json>' --writewhere <json> is { "surfaces": [{ "surfaceId", "surface", "current", "proposed",
"rationale", "evidenceRef" }] }. The verb scans the entire batch payload, every surface
field and property, not just proposed/rationale, and refuses the whole batch on any
current_base/comp guard hit anywhere in it — fix the offending surface and re-record.
This closes the Artifact Contract's two-places rule: the Documents doc plus this
on-record batch.
If profile_apply is off, you are done here — the document plus the recorded batch
is the deliverable. The user reviews the recorded suggestions in the app or applies edits by
hand. Do not push to write-back.
STEP 5 — APPLY THE APPROVED EDITS (only if profile_apply is allowed)
Only with profile_apply allowed, and only after the user approves specific fields. This
is confirm-first PER FIELD, never bulk-auto: show the before→after for one surface,
get an explicit yes, write that one field, verify it, then move to the next. Nothing is
written silently.
If the in-app pass already staged fields, read the batch first
(careerrat data linkedin-proposals latest --status reviewed, or the default pending
when some are held) and take its approved surfaces as the
work-list. Staging supplies what to apply, never permission to skip
the live confirmation. Still show each before→after and get a live yes here before
writing that field, exactly as above.
Drive each surface live (snapshot/read before each action). Session-browser mechanics that the live run proved out — apply them tool-agnostically (describe the technique, never name a specific automation tool):
- Find fields by their accessible label, not by coordinate scroll. Edit modals often open scrolled to the bottom; scrolling by fixed offsets snaps past the target field. Use the browser's element-finder to locate the field (e.g. the Description textarea) by its label/role and act on it directly.
- Clear then type: focus the field, select-all, delete, then type the approved text.
- If typing drops characters (common on React-controlled inputs — you'll see mangled
output like dropped or merged characters): set the field's value via an injected script
using the element's native value setter and dispatch an
inputevent (and achangeevent) so the framework registers it, then re-read the field to confirm the clean value landed. This is the reliable fallback when direct typing corrupts the text. - After Save, skip any upsell the platform shows (e.g. a "people you may know" / connect prompt) — dismiss it, never take the upsell action.
- Verify by reloading and re-reading. List/summary views go stale after an edit; reload the page and read the field text back to confirm the change is live before reporting it done.
- Featured items are link cards. External links auto-pull a title/description that are then editable in the item's media-edit modal; edit the title/description there.
- Contact info supports multiple websites, each with a URL and a type/label; the email field is typically read-only (managed in account settings) — don't try to edit it here.
- Open-to-work / job-preferences title field is free-text and visible only to the user; editing the title can silently flip the job-alerts toggle, so re-check/restore that toggle after.
If a field's edit UI fights the automation, stop forcing it — hand that one field back
as a suggestion (it stays in the STEP 4 document) rather than risk a bad write. The same
honesty + current_base privacy invariants hold on everything written.
Never edit anything other than the candidate's own profile. This skill sends no messages, no connection requests, no reactions.
STEP 6 — VERIFY + LOG
For every field written in STEP 5, reload and re-read it live to confirm the new text is
present and correct (quote it back to the user). The before→after document from STEP 4
stays in ~/Documents/careerrat/ as the record of the pass — write it regardless of whether
anything was applied.
If the field was not already approved in the app, record the user's live yes first
(careerrat data linkedin-proposals approve --batch <id> --surface <surfaceId> --version <n>);
mark-applied only works on an approved surface.
For each field verified live, flip its batch decision to applied so the in-app card stops showing it as approved-but-unwritten:
careerrat data linkedin-proposals mark-applied --batch <id> --surface <surfaceId> --version <n>Only mark a field applied after the reload-and-re-read confirms it — never on the write attempt alone.
Every decision bumps the batch version, and the command above prints the updated batch
in its response. Each subsequent mark-applied call must use the version from the
PREVIOUS call's output, never the original batch's version — re-read the current version
via careerrat data linkedin-proposals latest --status any if you lost track of it.
Then log one event to the Activity Pulse feed (the dashboard's live timeline — see
Activity Pulse in AGENTS.md), actor agent:
careerrat activity append --type system --actor agent \
--title "LinkedIn profile pass" \
--summary "<N surfaces reviewed, M fields applied (or 'suggest-only')>" \
--url "<profile url>" --writeIf the pass surfaces nothing the candidate should act on, say so — don't manufacture changes. The point is an honest profile that reads for the target roles, not a longer one.
RULES
- On by default, two separate switches. Run a browser pass only where
careerrat automation status --jsonshows the capabilityallowed: trueforlinkedin.profile_optimizecovers reading + suggesting;profile_applyseparately covers write-back. Theallowedfield is themayRun()verdict fromsrc/core/automation/consent.mjs(neither switch is turned off); never re-derive it in prose. If a switch is off, say so with the enable command and skip that part. - Dry-run preview is the default and the first move. Always produce the full read-only before→after set before writing anything. Applying is a deliberate, per-field second step. The skill is a complete read-only audit on its own.
- Confirm-first per field on write. Editing a public profile = modifying public-facing content. Show the before→after for each field and wait for an explicit yes before writing it. Never bulk-auto. If a field's edit UI fights, hand it back as a suggestion.
- Honesty firewall (hard). Every proposed or written line traces to an
evidence.ymlclaim;honesty.ymloverclaim caps anddo_not_claim/do_not_fabricateapply; no fabrication. Profile copy is held to the same standard as a tailored résumé. The honest-vs-target field rule: honest descriptions in headline/About/experience, target titles only in open-to-work. - Stale evidence ≠ overclaim. A profile claim that doesn't trace to
evidence.ymlmay mean the evidence bank is out of date, not that the profile overclaims. Verify against the source repo/portfolio first; if it's true, updateevidence.ymlrather than cutting honest copy. An expired-but-earned credential is factual — keep it shown honestly (as expired), never forward-dated to look current. - Obfuscation / NDA. Internal employer projects are described generically (no codenames, no project names), sensitive specifics redacted, only safe public metrics kept. The candidate's own public portfolio / OSS work keeps its real names + real public stats. Source of truth for "what's already public" is the candidate's own surface, never a guess.
- Privacy invariant (hard).
compensation.current_basemust never appear in any suggested or written profile copy, or in any artifact underworkspace//~/Documents/careerrat/. Outbound-safe comp fields areexpected_base,target_base, andminimum_baseonly. - Don't "fix" intentional styling. A candidate's deliberate brand styling is a choice, not a bug — never flag it.
- Halt on auth challenges. On captcha, 2FA, login wall, or any unexpected interstitial, halt and ask the user to resolve it. Never bypass or automate around an auth challenge.
- Tool-agnostic browser prose. Say "the session browser," "the browser's
element-finder," "an injected script." Keep the provider on
auto; never ask the user to choose a CLI, extension, or browser driver, and never name an MCP namespace or vendor tool. - Local-only. Scraped profile text, screenshots, and the before→after document stay
under
workspace/or~/Documents/careerrat/. Nothing goes outbound. No credentials are stored — the session browser holds the login. - Read-only on everything but the candidate's own profile fields. No messages, no connection requests, no reactions, no edits to anyone else's content.
- No placeholder brackets. If a detail is unknown, go generic or omit it — never emit
[Company],[Role],[Name], or any bracket token anywhere. - Domain-neutral. No hardcoded companies, roles, titles, codenames, or
candidate-specific values in this skill. The skill is a field-agnostic procedure; what
makes it conform to this candidate is their
candidate/*.ymltruth bank.
In-app and terminal paths
CareerRat's in-app path reads the candidate's LinkedIn profile through its owned
browser workflow unless profile_optimize is turned off, then creates a
bounded, evidence-grounded proposal batch for review. A terminal or
external-agent run still follows STEP 0 onward exactly as written.
- Requesting a profile pass (
linkedin.optimize-request). "Optimize my LinkedIn" offers an "Optimize LinkedIn profile" chip. Withprofile_optimizeturned off, the handler says so, offers a "Turn it on" action, and leaves any existing proposal batch visible. When reading is allowed, it runs the native profile pass and renders the returned proposal batch for review. Pasted profile text remains available on the terminal or external-agent path. - Staged suggestions (
linkedin.proposal-decide). The in-app pass stages every suggestion that passes the checks on its own and saves it with an Undo. Each one shows as "Staged for your profile". A suggestion with a placeholder to fill in, or no evidence behind it, stays undecided for you to approve or reject. Undo returns a staged suggestion to undecided, and it refuses once the field was written to LinkedIn. A stale click (the batch changed underneath) refuses. Once every suggestion is decided the batch is reviewed, so read it withlinkedin-proposals latest --status reviewedas well as the default pending. - Write boundary. Staging a suggestion in the app is local and automatic; no
live LinkedIn field changes when it is staged. Applying approved edits live remains
behind the separate
profile_applyswitch and a per-field confirmation on the terminal or external-agent STEP 5 path. STEP 6 verifies the live result and marks only verified fields applied so the card stays honest.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

