Share an Instagram or Facebook post from your Android phone and get it back as a clean, share-ready pack: every carousel image as a JPEG (long edge at most 1568 px, quality 85), key frames and a transcript for videos, and the caption as text. Then pass it straight on to an AI chat app, a messenger or anywhere else in the share menu.
No more screenshots of every slide, no more screen recordings.
Status: personal project, published in case it is useful to others. It works for me, but comes with no support promise and may break whenever Instagram or Facebook change something. Use at your own risk.
Instagram / Facebook app
│ Share → Sharewerk (Android share sheet)
▼
Sharewerk PWA on your phone ──link──▶ Sharewerk server (Docker)
├─ gallery-dl / yt-dlp download the post
├─ Pillow images → JPEG, 1568 px
├─ ffmpeg video → key frames, audio
└─ faster-whisper audio → transcript
◀──── images + post.txt ───────────────┘
│ Share → Claude, ChatGPT, WhatsApp, … (Android share sheet)
Sharewerk is a Progressive Web App (a website you install like an app). Once installed through Chrome, it registers as a share target, so it appears in Android's share menu. The phone only sends the link; all downloading and processing happens on your server.
What you get for a post:
| Post contains | Pack contains |
|---|---|
| Image carousel | Every image as JPEG (long edge at most 1568 px, quality 85: sharp enough to read text, small enough to share fast) |
| Video / reel | Key frames at scene changes plus a timestamped transcript |
| Anything | A post.txt with source, author, date, caption and an index of all pictures |
Android allows at most 10 files per share. If a post has more pictures than fit, Sharewerk combines them into numbered contact sheets so the whole post still goes out in one share (and lands in one chat). Single pictures and the original media files can be shared separately.
iPhone is not supported: Safari does not implement the Web Share Target API, so a web app cannot appear in the iOS share sheet.
- A Linux machine with Docker that runs all the time: a small server, or a computer at home (2 CPU cores and 4 GB RAM are plenty)
- HTTPS in front of it (PWAs only install from secure origins): a domain, or Tailscale
- An Android phone with Chrome, and Chrome on a computer for the browser extension
- For Instagram: a secondary Instagram account (see Cookies)
docs/getting-started.md walks you through it step by step, no Docker or web server knowledge needed. setup.sh does the work for one of two ways:
git clone https://github.com/Azentrius/sharewerk.git
cd sharewerk
./setup.sh --domain sharewerk.example.com # own server: Caddy gets the HTTPS certificate itself
./setup.sh --tailscale tskey-auth-... # or at home: reachable in your tailnet onlyIt writes .env with a new access token and COMPOSE_FILE (so plain docker compose commands include compose.caddy.yaml or compose.tailscale.yaml), creates secrets/, starts Sharewerk and shows the address and the token.
Without setup.sh:
git clone https://github.com/Azentrius/sharewerk.git
cd sharewerk
cp .env.example .env
sed -i "s/^SHAREWERK_AUTH_TOKEN=.*/SHAREWERK_AUTH_TOKEN=$(openssl rand -hex 32)/" .env
mkdir -p secrets
docker compose up -d
docker compose logs -f sharewerkThe service now listens on 127.0.0.1:8080. Put a TLS reverse proxy in front of it, for example Caddy:
sharewerk.example.com {
reverse_proxy 127.0.0.1:8080
}Open https://sharewerk.example.com on your phone, enter the token from .env once, then install the app (see Install on Android).
The first video you process downloads the Whisper speech model (about 500 MB for small) into the data volume. That run takes a bit longer.
Instagram shows almost nothing to logged-out visitors, so the downloaders need the cookies of a logged-in account. Facebook videos often work without cookies; Facebook posts that only logged-in accounts can see need them too.
Use a secondary account, not your main one. Automated access violates Meta's terms of use, and accounts used this way can be rate-limited or blocked. To make that less likely, Sharewerk waits SHAREWERK_INSTAGRAM_PAUSE seconds (20 by default) between the starts of two Instagram downloads; the page says so while a post waits.
There are two ways to give Sharewerk the cookies: the browser extension sends them whenever you log in, or you put a cookie file on the server by hand. Sharewerk reads them again for every job, so there is no need to restart it. Each download gets only the cookies of its own platform, and cookies sent by the extension take precedence over the file.
Cookies are a logged-in session. Treat them like a password. secrets/ is git-ignored, and the gitleaks configuration in this repository detects Instagram/Facebook session cookies if one ever ends up in a commit.
The extension in extension/ sends the cookies of Instagram and Facebook to your Sharewerk server when you log in there, and again whenever the login changes. It uses your login at Sharewerk in the same browser (the token login, or your proxy's such as Pomerium), so it needs no secret of its own.
- Create a separate Chrome profile for the secondary accounts and do nothing else in it.
- Download
sharewerk-extension-X.Y.Z.zipfrom the latest release and unpack it, or build it yourself withpython tools/build-extension.py(the result is indist/). - In that profile open
chrome://extensions, switch on Developer mode, click Load unpacked and pick the unpacked folder. - The settings page opens: enter your Sharewerk address and allow the access Chrome asks for.
- Sign in to Sharewerk in this profile, then log in to Instagram, and to Facebook if you want it.
- Open the extension's menu (its icon in the toolbar) and confirm the account with Use account. From then on the extension sends that account's cookies by itself. Another account in the same browser, such as your main one, is never sent without asking again.
The icon shows ? while an account waits for your confirmation and ! when the cookies cannot reach Sharewerk (no address, no login at Sharewerk, server unreachable or an error); the menu says which and what to do. When the login at your proxy expires (Pomerium: cookie_expire), the extension waits until you sign in to Sharewerk again in that profile, then sends what waited.
What the extension can do: read the cookies of instagram.com and facebook.com, and send them to the one address you entered. It runs no code on web pages and loads no code from the internet. Sharewerk keeps the cookies in the data volume (/data/cookies/<platform>.txt, readable by the container user only) and never writes their values to a log. In the settings page you can delete them from Sharewerk again; the extension then asks before it sends an account again. PRIVACY.md sums up what the extension, the server and the app do with data.
- Create a separate browser profile, log in to the secondary account there, and do nothing else in that profile. This keeps the cookie file limited to that one account.
- Export the cookies in Netscape
cookies.txtformat, for example with the browser extension "Get cookies.txt LOCALLY" (Chrome) or "cookies.txt" (Firefox). In Chrome, take the one with "LOCALLY" in its name: an older extension called "Get cookies.txt" was removed from the Chrome Web Store as malware. Export instagram.com, and facebook.com if you want it. - Close the browser profile without logging out (logging out invalidates the cookies).
- Copy the file to the server, put it in place so that only the container user (uid 10001) can read it, and delete the copy:
sudo install -o 10001 -g 10001 -m 600 cookies.txt secrets/cookies.txt
shred -u cookies.txtDelete the exported file on your computer as well. Cookies expire after a while, or sooner when Instagram gets suspicious. When Sharewerk reports "Instagram asks for a login", export them again.
- Open your Sharewerk URL in Chrome and sign in.
- Chrome menu ⋮ → Install app (on some versions: "Add to home screen").
- In Instagram tap Share → Share to… / More, pick Sharewerk.
- When the pack is ready, tap Share and pick the target app.
If Sharewerk does not appear in the share menu, it was added as a plain bookmark rather than installed. Remove it and install again from Chrome's menu.
If you already have an identity-aware proxy (Pomerium, oauth2-proxy, Authelia, Cloudflare Access …), let it handle logins and switch Sharewerk's own token off:
SHAREWERK_AUTH_MODE=noneOnly do this if the port is unreachable except through the proxy. Two things are already taken care of for proxies:
- The share target uses
GET, so the shared link survives a redirect to the proxy's login page and back. - The manifest is loaded with credentials (
crossorigin="use-credentials"), so Chrome can install the app through the proxy. If your proxy still blocks it, make/manifest.webmanifest,/sw.jsand/static/*public; they contain nothing private.
Example Pomerium routes, with a public health check for monitoring:
routes:
- from: https://sharewerk.example.com
path: /health
prefix_rewrite: /healthz
to: http://10.0.0.5:8080
allow_public_unauthenticated_access: true
- from: https://sharewerk.example.com
to: http://10.0.0.5:8080
timeout: 600s
policy:
- allow:
or:
- email:
is: you@example.comHere Pomerium runs on another machine and reaches Sharewerk at the private address 10.0.0.5, but compose.yaml binds the port to 127.0.0.1 only: change the binding to that private address (never a public one, see the comment in compose.yaml), or use http://127.0.0.1:8080 when Pomerium runs on the same machine.
Sharewerk answers GET /healthz with {"ok":true,"cookies":"ok","cookie_states":{"instagram":"ok","facebook":"missing"}}. Pomerium, however, answers /healthz and /ping itself, so a monitor on those paths only shows that Pomerium is up. The first route passes /health to Sharewerk's /healthz without a login; point your monitor there and check for "ok":true. Everything else stays behind the policy. Keep the public route first, because Pomerium uses the first route that matches. The longer timeout gives large files time to download over a slow connection; Pomerium's default is 30 seconds.
The health check has two parts:
"ok"isfalse(with HTTP 503) when posts cannot be processed at all:worker_stoppedorstorage_failedin"problem"."cookies"says what the last Instagram download found:ok,expired(Instagram asked for a login),missing(no Instagram cookies, neither sent by the extension nor in the cookie file) orunknown(no Instagram download started since the cookies were last written, e.g. right after you logged in again or exported new cookies). It never turns the check red, because Facebook works without cookies."cookie_states"has the same for every platform. For Facebook,expiredcan also mean that the last post was visible to its author's friends only.
For a warning when the cookies run out, add a second keyword monitor on the same URL that alerts when it finds "cookies":"expired" (in Uptime Kuma: keyword monitor with "Invert Keyword"). After a restart the state comes from the jobs still kept (SHAREWERK_RETENTION_HOURS).
Instagram and Facebook change constantly; when downloads start failing, a newer gallery-dl or yt-dlp almost always fixes it. The image ghcr.io/azentrius/sharewerk:latest is rebuilt every week with the newest versions, so updating is:
docker compose pull && docker compose up -dIf you build locally: docker compose build --pull --no-cache && docker compose up -d.
Images carry a signed build provenance; check one with gh attestation verify oci://ghcr.io/azentrius/sharewerk:latest --repo Azentrius/sharewerk.
| Tag | What it is |
|---|---|
latest |
The newest build of main, rebuilt on every change and every Monday with the newest downloaders. Use this one. |
X.Y.Z (e.g. 0.1.0) |
A release. It keeps the downloaders of its build day and never changes. |
YYYYMMDD |
The Monday rebuild of that day. |
sha-<commit> |
The build of one commit on main. |
Every image carries the OCI labels org.opencontainers.image.version (the version in app/__init__.py, which is the last release, also on latest), org.opencontainers.image.revision (the commit) and org.opencontainers.image.source. Image update monitors can match the version label to the release tag, and show a newer build under latest as an update.
All settings are environment variables in .env. .env.example documents each one. The most important:
| Variable | Default | Meaning |
|---|---|---|
SHAREWERK_AUTH_MODE |
token |
token = own login, none = behind an auth proxy |
SHAREWERK_AUTH_TOKEN |
– | Access token, at least 24 characters |
SHAREWERK_WHISPER_MODEL |
small |
tiny … large-v3, or off |
SHAREWERK_WHISPER_LANGUAGE |
auto | Force a language, e.g. de |
SHAREWERK_IMAGE_MAX_EDGE |
1568 |
Long edge of shared images in px |
SHAREWERK_MAX_FRAMES |
8 |
Key frames per video |
SHAREWERK_RETENTION_HOURS |
24 |
Results are deleted afterwards |
SHAREWERK_INSTAGRAM_PAUSE |
20 |
Seconds between the starts of two Instagram downloads (0 = no pause) |
- The downloaders only ever receive URLs on an allowlist of Instagram and Facebook hosts (
SHAREWERK_ALLOWED_HOSTS). This prevents the server from being used to fetch arbitrary or internal addresses. - Never run Sharewerk as an open service for other people. The cookies are yours, and so is the responsibility for what is downloaded with them.
- With
SHAREWERK_AUTH_MODE=token, a device that entered the token keeps the login cookiesharewerk_sessionfor 400 days (HttpOnly, SameSite=Lax, Secure unlessSHAREWERK_COOKIE_SECURE=false). Its value is an HMAC-SHA256 of the token, the same on every device, not a session the server could end:POST /logoutdeletes it in that browser only, and the only way to lock out a lost device is a newSHAREWERK_AUTH_TOKEN, which signs out every device. Requests can also send the token itself asAuthorization: Bearer <token>. /healthzanswers without a login, also in token mode (as do/login,/manifest.webmanifest,/sw.js,/favicon.icoand/static/). It shows whether posts can be processed and the state of the Instagram and Facebook cookies (ok,expired,missing,unknown), never their values.- Only the browser extension sends cookies (
PUT /api/cookies/<platform>), with your login at Sharewerk and a header that a web page cannot add to a request to another site, so no other site can plant cookies through your browser. Sharewerk takes only cookies of the platform's own domain and refuses anything that could break the cookie file. Keep/api/cookiesbehind your proxy's policy, like everything except/health. - Where the Instagram and Facebook cookies are: the cookie file in
secrets/, mounted read-only; the cookies sent by the extension in the data volume (/data/cookies/<platform>.txt, mode 600) until you delete them in its settings page or new ones replace them; and, for each download, a copy of the post's platform's cookies in the job's folder (/data/jobs/<id>/cookies.txt, mode 600), because gallery-dl and yt-dlp write back into the file they get. Sharewerk deletes that copy when the download ends, also after an error. If Sharewerk stops in the middle of a download (restart, crash), the copy stays until the job is deleted: afterSHAREWERK_RETENTION_HOURS, or with the trash icon. - The container runs as a non-root user with a read-only root filesystem and no Linux capabilities.
- Results are deleted after
SHAREWERK_RETENTION_HOURS, or right away with the trash icon in "Recently prepared" (DELETE /api/jobs/<id>; a post that is still being prepared cannot be deleted).
Sharewerk is a tool for personal use: it saves you from taking screenshots of posts you can already see. Automated access to Instagram and Facebook is against Meta's terms of use, and the content of a post belongs to its author. You are responsible for how you use it. Passing a post on to other apps, such as AI chat apps or messengers, hands its content to these services and their providers; whether you may do that depends on the author's rights and on the terms of these services. The only cookie Sharewerk sets is its own login cookie in token mode (sharewerk_session), which only keeps a device signed in and is not used for tracking. This project is not affiliated with or endorsed by Meta Platforms. Instagram, Facebook and the other app names in this README are trademarks of their respective owners.
| Message | Usual cause | Fix |
|---|---|---|
Instagram asks for a login, or "cookies":"expired" in /healthz |
Cookies missing or expired | Log in to Instagram again in the extension's browser profile, or export new cookies; the state turns unknown as soon as new cookies arrive, ok after the next Instagram download |
| The post could not be downloaded | Downloader outdated | Pull or rebuild the image |
| Facebook asks for a login for this post | The post is visible to logged-in accounts only; Sharewerk has no (valid) Facebook cookies | Log in to a secondary Facebook account in the extension's browser profile, or export its cookies as well |
The extension's icon shows !, "Sharewerk asks for a login" |
No login at Sharewerk in that browser profile, or it expired | Open Sharewerk in that profile and sign in; the extension then sends by itself |
The extension's icon shows ? |
An account waits for your confirmation | Open the extension's menu; confirm with "Use account" only if it is the secondary account |
| Too many requests to Instagram or Facebook | Rate limit on the account | Wait, then try again |
| This pack is incomplete | The download stopped after some items, often at a rate limit; you get what arrived, and post.txt says so |
Prepare the post again later for the rest |
| Not in share menu | App not installed | Install from Chrome's menu |
| The transcript says "no speech detected; probably music only" | No speech in the audio | Expected for music reels |
| The server could not save the post | Data volume full or read-only | Free up space, then prepare the post again |
/healthz answers 503 |
worker_stopped: the job worker ended; storage_failed: job states cannot be saved |
Check the logs and the data volume, restart Sharewerk |
Logs: docker compose logs -f sharewerk
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
ruff check
python -m pytest
node --test tests/js/app.test.mjs tests/js/pwa-update.test.mjs tests/js/extension.test.mjs # no npm packages needed; pwa-update runs `python` (or $PYTHON) with the app's dependencies
SHAREWERK_AUTH_MODE=none SHAREWERK_HOST=127.0.0.1 SHAREWERK_DATA_DIR=./data SHAREWERK_COOKIE_SECURE=false python -m app.main # no login: this computer only
python tools/build-extension.py # the browser extension, in dist/
python tools/store-images.py # its Chrome Web Store images (needs Chrome or Edge), see docs/chrome-web-store.mdInstall the secret-scanning pre-commit hook once: pipx install pre-commit && pre-commit install.
Changes are listed in CHANGELOG.md; the version lives in app/__init__.py. Releases are made by the release workflow from main.
Code layout: app/urls.py (link validation), app/fetch.py (downloaders), app/cookies.py (cookies per platform), app/process.py (images, frames, contact sheets, text), app/transcribe.py (Whisper), app/jobs.py (queue), app/main.py (HTTP), app/static/ (PWA), app/static/azentrius/ (design system, copied unchanged), extension/ (browser extension; tools/build-extension.py adds the design system, theme.js, icons.js and the license texts from the app), setup.sh with compose.caddy.yaml, compose.tailscale.yaml and deploy/ (the two ways of docs/getting-started.md; CI starts the Caddy way with a local certificate, tests/ci/).
The interface follows the Azentrius Design System, theme "Azentrius hell" or "Azentrius dunkel" depending on the device setting. app/static/azentrius/ holds the system's tokens.css, bundle.css and font files, copied unchanged; never edit them by hand, replace them with a newer copy of the design system instead. app/static/app.css contains only layout the design system leaves to the app, and extension/extension.css the same for the extension, which the build gives the same copies.
Sharewerk is licensed under the Apache License 2.0; see also NOTICE. Not covered by it: the Azentrius brand, i.e. the wordmark "Azentrius.", its signs and the product names, including "Sharewerk" and the Sharewerk sign (all rights reserved).
The name and the sign. You may run your own Sharewerk, unmodified or with your own settings, under its name and with its sign, without asking. Please keep the footer with the name and the link to the source code. If you change the code and run or publish the changed version, give it another name and sign, so nobody takes it for Sharewerk; you may say that it is based on Sharewerk.
Bundled with their own licenses (texts in LICENSES/): the design system's CSS (Apache-2.0), the fonts DM Sans, DM Serif Display and Red Hat Mono (SIL OFL 1.1), and the icons (Font Awesome Free 5 under CC BY 4.0, one PatternFly icon under MIT). The Docker image additionally contains GPL-licensed gallery-dl and FFmpeg; see NOTICE.
Made by Azentrius.