Install
openclaw skills install localsendSend and receive files to/from nearby devices using the LocalSend protocol. Trigger with /localsend to get an interactive Telegram menu with real inline butt...
openclaw skills install localsendInteractive file transfer between devices on the local network using real Telegram inline keyboard buttons. Works with any device running the LocalSend app (Android, iOS, Windows, macOS, Linux).
The localsend-cli is a zero-dependency Python CLI. Install from GitHub:
curl -fsSL https://raw.githubusercontent.com/Chordlini/localsend-cli/master/localsend-cli -o ~/.local/bin/localsend-cli
chmod +x ~/.local/bin/localsend-cli
Full docs: https://github.com/Chordlini/localsend-cli
Requires Python 3.8+ and openssl (for TLS).
All menus MUST use OpenClaw's inline button format. Send buttons alongside your message using this structure:
buttons: [
[{ "text": "Label", "callback_data": "ls:action" }],
[{ "text": "Row 2", "callback_data": "ls:other" }]
]
ls: to namespace this skillcallback_data: ls:actionThis skill uses conversational state. Track where you are in the flow:
| State | Meaning | Next user input should be treated as... |
|---|---|---|
idle | No active flow | Normal message — respond normally |
awaiting_file | Asked user to drop/specify a file to send | The file to send — do NOT comment on it, describe it, or react to it. Immediately use it as the send payload. |
awaiting_text | Asked user to type text to send | The text payload — send it, don't discuss it |
awaiting_confirm | Waiting for send confirmation | Expect ls:confirm-send or ls:menu |
receiving | Receiver is active | Monitor for incoming files |
RULES:
awaiting_file state and user sends an image/file/path → treat it as the file to send. Show confirmation buttons immediately.awaiting_text state and user types anything → treat it as the text to send.awaiting_file state.idle when user taps ls:menu or the flow completes.When the user types /localsend or mentions sending/receiving files locally, send this message with real inline buttons:
Message:
📡 LocalSend — File Transfer
Buttons:
buttons: [
[
{ "text": "📤 Send", "callback_data": "ls:send" },
{ "text": "📥 Receive", "callback_data": "ls:receive" }
],
[
{ "text": "🔍 Scan Devices", "callback_data": "ls:devices" }
]
]
Do NOT run any commands yet. Wait for the button tap.
Trigger: callback_data: ls:devices or user says "scan", "discover", "find devices"
Run:
localsend-cli discover --json -t 2
Devices found — create one button per device, plus Refresh and Back:
Message:
📡 Found 3 devices:
Buttons (one device per row):
buttons: [
[{ "text": "📱 Fast Potato — 192.168.0.148", "callback_data": "ls:dev:Fast Potato" }],
[{ "text": "💻 Rami-Desktop — 192.168.0.100", "callback_data": "ls:dev:Rami-Desktop" }],
[{ "text": "🖥️ Living Room PC — 192.168.0.105", "callback_data": "ls:dev:Living Room PC" }],
[
{ "text": "🔄 Refresh", "callback_data": "ls:devices" },
{ "text": "⬅️ Back", "callback_data": "ls:menu" }
]
]
No devices found:
Message:
📡 No devices found.
Make sure LocalSend is open on the other device and both are on the same WiFi.
Buttons:
buttons: [
[
{ "text": "🔄 Try Again", "callback_data": "ls:devices" },
{ "text": "⬅️ Back", "callback_data": "ls:menu" }
]
]
User taps a device (callback_data: ls:dev:DEVICENAME) — store it as the selected target. Show action menu:
Message:
✅ Selected: Fast Potato (192.168.0.148)
What do you want to do?
Buttons:
buttons: [
[
{ "text": "📄 Send File", "callback_data": "ls:sendfile" },
{ "text": "📝 Send Text", "callback_data": "ls:sendtext" }
],
[
{ "text": "📦 Send Multiple", "callback_data": "ls:sendmulti" },
{ "text": "⬅️ Back", "callback_data": "ls:devices" }
]
]
Trigger: callback_data: ls:send
Run discover and show device picker (see Scan Devices flow above).
Message:
Send to Fast Potato:
Buttons:
buttons: [
[
{ "text": "📄 Send File", "callback_data": "ls:sendfile" },
{ "text": "📝 Send Text", "callback_data": "ls:sendtext" }
],
[
{ "text": "📦 Send Multiple", "callback_data": "ls:sendmulti" },
{ "text": "⬅️ Back", "callback_data": "ls:menu" }
]
]
callback_data: ls:sendfile)Ask: "Send me the file, drop a path, or tell me which file to send"
User provides file path or sends a file via chat
Get file size with stat or ls -lh
Confirm with buttons:
Message:
📤 Send to Fast Potato?
📄 project.zip — 4.2 MB
Buttons:
buttons: [
[
{ "text": "✅ Send", "callback_data": "ls:confirm-send" },
{ "text": "❌ Cancel", "callback_data": "ls:menu" }
]
]
On confirm, run:
localsend-cli send --to "Fast Potato" /path/to/project.zip
Report result:
Message:
✅ Sent project.zip (4.2 MB) to Fast Potato
Buttons:
buttons: [
[
{ "text": "📤 Send Another", "callback_data": "ls:send" },
{ "text": "⬅️ Menu", "callback_data": "ls:menu" }
]
]
callback_data: ls:sendtext)Ask: "Type the text you want to send:"
User types their message
Write text to temp file, send:
echo "user's text" > /tmp/localsend-text.txt
localsend-cli send --to "Fast Potato" /tmp/localsend-text.txt
rm /tmp/localsend-text.txt
Confirm:
Message:
✅ Text sent to Fast Potato
Buttons:
buttons: [
[
{ "text": "📝 Send More Text", "callback_data": "ls:sendtext" },
{ "text": "📤 Send File", "callback_data": "ls:sendfile" }
],
[{ "text": "⬅️ Menu", "callback_data": "ls:menu" }]
]
callback_data: ls:sendmulti)Ask: "List the files or give me a glob pattern (e.g. ~/Screenshots/*.png)"
User provides paths or pattern
Expand glob, list files with sizes:
Message:
📦 Send 5 files to Fast Potato?
📄 photo1.jpg — 2.1 MB
📄 photo2.jpg — 1.8 MB
📄 photo3.jpg — 3.2 MB
📄 photo4.jpg — 2.5 MB
📄 photo5.jpg — 1.9 MB
📊 Total: 11.5 MB
Buttons:
buttons: [
[
{ "text": "✅ Send All", "callback_data": "ls:confirm-send" },
{ "text": "❌ Cancel", "callback_data": "ls:menu" }
]
]
On confirm, run:
localsend-cli send --to "Fast Potato" photo1.jpg photo2.jpg photo3.jpg photo4.jpg photo5.jpg
Report:
Message:
✅ Sent 5 files (11.5 MB) to Fast Potato
Buttons:
buttons: [
[
{ "text": "📤 Send More", "callback_data": "ls:send" },
{ "text": "⬅️ Menu", "callback_data": "ls:menu" }
]
]
Trigger: callback_data: ls:receive or user says "receive", "start receiving", "listen"
ls -1 /home/rami/.openclaw/workspace/_incoming/ > /tmp/localsend-before.txt
localsend-cli --alias openclaw-workspace receive --save-dir /home/rami/.openclaw/workspace/_incoming/ -y
Run with run_in_background: true. Store the task ID.
CRITICAL: --alias MUST come BEFORE receive (global flag).
Message:
📡 Receiver active — "openclaw-workspace"
📁 Saving to: ~/incoming/
✅ Auto-accept: ON
Send files from your device whenever ready.
Buttons:
buttons: [
[
{ "text": "🛑 Stop", "callback_data": "ls:stop" },
{ "text": "🔄 Status", "callback_data": "ls:status" }
]
]
Poll every 3 seconds for new files:
ls -1 /home/rami/.openclaw/workspace/_incoming/ > /tmp/localsend-after.txt
diff /tmp/localsend-before.txt /tmp/localsend-after.txt
When file(s) arrive, immediately present in chat with inline buttons.
Single file:
Message:
✅ Received from Fast Potato:
📄 portfolio.zip — 240 MB
📁 Saved to: ~/incoming/portfolio.zip
Buttons (contextual by file type):
buttons: [
[
{ "text": "📂 Extract", "callback_data": "ls:extract" },
{ "text": "🚀 Deploy", "callback_data": "ls:deploy" }
],
[
{ "text": "📥 Receive More", "callback_data": "ls:receive" },
{ "text": "🛑 Stop", "callback_data": "ls:stop" }
]
]
Image file — show inline preview:
Message:
✅ Received from Fast Potato:
🖼️ screenshot.png — 2.1 MB
Include MEDIA:~/incoming/screenshot.png for inline preview.
Buttons:
buttons: [
[
{ "text": "📂 Open Folder", "callback_data": "ls:openfolder" },
{ "text": "📥 Receive More", "callback_data": "ls:receive" }
],
[{ "text": "🛑 Stop", "callback_data": "ls:stop" }]
]
Multiple files:
Message:
✅ Received 3 files from Fast Potato:
📄 app.apk — 45 MB
📄 README.md — 2 KB
🖼️ icon.png — 128 KB
📊 Total: 45.1 MB
Buttons:
buttons: [
[
{ "text": "📂 Show All", "callback_data": "ls:showall" },
{ "text": "📥 Receive More", "callback_data": "ls:receive" }
],
[{ "text": "🛑 Stop", "callback_data": "ls:stop" }]
]
Contextual button rules by file type:
.zip, .tar.gz → add 📂 Extract button.png, .jpg, .gif, .webp → show MEDIA: inline + 📂 Open Folder.apk → add 📱 Install button.html, .js, .py → add 👁️ Preview button🚀 Deploy buttonTrigger: callback_data: ls:stop
Kill the background task using stored task ID
Confirm:
Message:
🛑 Receiver stopped.
Buttons:
buttons: [
[
{ "text": "📡 Restart", "callback_data": "ls:receive" },
{ "text": "⬅️ Menu", "callback_data": "ls:menu" }
]
]
Trigger: callback_data: ls:status
Check if receiver is running and count new files:
ls -1 /home/rami/.openclaw/workspace/_incoming/ > /tmp/localsend-after.txt
diff /tmp/localsend-before.txt /tmp/localsend-after.txt | grep "^>" | wc -l
Message:
📡 Receiver: Running (12 min)
📁 Files received: 2
📊 Total: 242 MB
Buttons:
buttons: [
[
{ "text": "🛑 Stop", "callback_data": "ls:stop" },
{ "text": "📂 Show Files", "callback_data": "ls:showall" }
]
]
| callback_data | Action |
|---|---|
ls:menu | Show main menu |
ls:send | Start send flow |
ls:receive | Start receive flow |
ls:devices | Discover devices |
ls:dev:DEVICENAME | Select a specific device |
ls:sendfile | Send single file |
ls:sendtext | Send text message |
ls:sendmulti | Send multiple files |
ls:confirm-send | Confirm and execute send |
ls:stop | Stop receiver |
ls:status | Check receiver status |
ls:extract | Extract received archive |
ls:deploy | Deploy received website |
ls:openfolder | Open save directory |
ls:showall | List all received files |
| Command | Usage |
|---|---|
| Discover | localsend-cli discover --json -t 2 |
| Send | localsend-cli send --to "DEVICE" file1 file2 ... |
| Receive | localsend-cli --alias NAME receive --save-dir DIR -y |
| Flag | Scope | Description |
|---|---|---|
--alias NAME | Global (before subcommand) | Device name to advertise |
--to NAME | send | Target device (case-insensitive substring) |
-t N | discover | Scan duration in seconds (use 2 for speed) |
--json | discover | Machine-readable output |
--save-dir DIR | receive | Save location (default: ~/Downloads) |
-y | receive | Auto-accept transfers |
| Problem | Fix |
|---|---|
unrecognized arguments: --alias | Move --alias BEFORE the subcommand |
| No devices found | Open LocalSend on target, same WiFi, screen on |
| Port 53317 in use | Normal — CLI auto-falls back to 53318/53319 |
| Transfer declined (403) | Use -y on receiver side |
| Transfer hangs | Large file on slow WiFi — be patient |
references/protocol.md or https://github.com/localsend/protocol