You have something in a terminal. Someone else needs to see it in a browser. The usual routes are all slightly wrong: pasting a 200-line stack trace into Slack buries the channel, email mangles the whitespace, and a screenshot of text is a small cruelty.
What you actually want is to turn stdout into a URL. Here is the whole thing:
curl -X POST https://sharenotes.dev/api/v1/notes \
-H "Content-Type: text/plain" \
--data-binary @error.log
That returns JSON with a link you can paste anywhere:
{"slug":"blue-fox-42","url":"https://sharenotes.dev/blue-fox-42","expiresAt":null,"burnAfterRead":false}
No API key. No account. No SDK to install. The rate limit is 20 notes per day per IP, which is far more than an interactive workflow needs.
Piping, which is the actual point
--data-binary @- reads stdin, so anything that writes to stdout can
become a link:
# the diff you want a second opinion on
git diff | curl -X POST https://sharenotes.dev/api/v1/notes \
-H "Content-Type: text/plain" --data-binary @-
# the last 200 lines of a failing service
journalctl -u myapp -n 200 --no-pager | curl -X POST ... --data-binary @-
# a pod that will not start
kubectl logs deploy/api --tail=500 | curl -X POST ... --data-binary @-
# test output, with colour codes stripped so it stays readable
npm test 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | curl -X POST ... --data-binary @-
Strip ANSI colour before piping. Most test runners and build tools
emit escape codes when they think a terminal is attached. They survive the pipe and
render as noise. The sed above removes them; many tools also accept a
--no-color flag, which is cleaner.
Make it a shell function
Typing the full curl every time gets old. Drop this in your .bashrc or
.zshrc:
share() {
local body
if [ $# -gt 0 ]; then
# Arguments given — treat the first as a file path, else as literal text.
body=$([ -f "$1" ] && cat "$1" || printf '%s' "$*")
else
body=$(cat) # nothing passed, so read stdin
fi
printf '%s' "$body" \
| curl -sS -X POST https://sharenotes.dev/api/v1/notes \
-H "Content-Type: text/plain" --data-binary @- \
| grep -o '"url":"[^"]*"' | cut -d'"' -f4
}
Now all three of these work, and print just the URL:
git diff | share
share error.log
share "the wifi password is hunter2"
Pipe it to your clipboard to skip the copy step — | pbcopy on macOS,
| xclip -sel clip on Linux, | clip.exe under WSL.
Options worth knowing
Send JSON instead of plain text when you want more than the default:
curl -X POST https://sharenotes.dev/api/v1/notes \
-H "Content-Type: application/json" \
-d '{
"content": "DATABASE_URL=postgres://...",
"expiresIn": "1h",
"burnAfterRead": true,
"title": "staging env"
}'
| Field | What it does |
|---|---|
expiresIn | 1h, 24h, 7d or 30d. The note hard-deletes itself after that. |
burnAfterRead | true destroys the note the first time it is read. |
customSlug | 3–10 characters, so you can pick sharenotes.dev/ci-log. |
password | A 4–6 digit PIN, with passwordMode of edit or view_edit. |
title | Up to 100 characters, shown in the note view. |
Reading one back is a plain GET, which makes it usable as a crude config drop:
curl -s https://sharenotes.dev/api/v1/notes/blue-fox-42 | jq -r .content
Using it from CI
A failing build that posts its own log is a nice trick. In a GitHub Actions step:
- name: Publish failure log
if: failure()
run: |
URL=$(tail -c 100000 build.log \
| jq -Rs '{content: ., expiresIn: "7d"}' \
| curl -sS -X POST https://sharenotes.dev/api/v1/notes \
-H "Content-Type: application/json" --data-binary @- \
| jq -r .url)
echo "Build log: $URL" >> $GITHUB_STEP_SUMMARY
Two things to note. jq -Rs wraps arbitrary text as a JSON string, which
is the safe way to build the body — hand-rolled string interpolation breaks the moment
your log contains a quote or a newline. And tail -c caps the size: the
limit is 10MB per note, and a runaway log will blow past that.
CI runners share egress IPs. The 20-notes-per-day limit is per IP, so a busy hosted runner pool can hit it collectively. Post on failure only, rather than on every run.
What not to pipe
The convenience makes it easy to be careless, so, plainly:
- Notes are not end-to-end encrypted. They are stored server-side.
burnAfterReadand short expiries limit how long a copy lingers; they are not a substitute for a secrets manager. - Anyone with the URL can open it. The slug is unguessable in practice, but the link is the credential. Add a PIN if that matters.
- Rotate anything you do send. If a staging token goes into a paste to unblock a colleague, treat it as burnt and cycle it afterwards.
- Watch what is already in your scrollback.
env | shareis a very fast way to publish every secret on the machine.
Why a URL beats the alternatives
Chat apps truncate long pastes and destroy indentation. A gist needs a GitHub account, and anonymous gists cannot be deleted by whoever made them. Screenshots are not searchable, not copyable, and not accessible. A plain link with the text intact avoids all three, and if you set an expiry it cleans up after itself.
The full reference — every field, every error code, the rate limits — is on the developer page. If you would rather not use a terminal at all, the editor does the same thing with a paste and a click.