CLI tool
fieu/discord.sh avatar
fieu/discord.sh

discord.sh posts to a Discord webhook from pure Bash

Write-only command-line Discord webhooks integration written in 100% Bash script

564 stars78 forksShellGPL-3.0

At a glance

What is it?
discord.sh is a write-only command line integration for Discord webhooks, written entirely in Bash in under 600 lines and depending only on curl and jq at run time. It suits a shell script or CI job that needs to say something in a channel, and it is one direction only by design.
Who is it for?
Adopt discord.sh if you need to send a formatted message to a Discord channel from a shell script, a cron job or a CI step, and you would rather not add a Node or Python runtime to the machine. Do not adopt it if you need to read from Discord, since the project describes itself as write-only and there is no command to fetch messages.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 15 days ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One direction, one file, no runtime

discord.sh is described as a write-only command-line integration for Discord webhooks, written in 100% Bash script and heavily influenced by an existing Slack command line tool. The word write-only is the key constraint and it is stated in the project's own summary rather than being a limitation left for a user to discover. There is no command to list channels, no command to read history and no long-lived session, so this is a tool for sending, not for talking. The scale claim is the other thing to note: less than 600 lines of pure Bash, described as extremely lightweight, and the dependency list for running it is only curl and jq. The repository layout matches the description, with a discord.sh script, a tests/ directory, a Makefile, an assets directory holding the images used in the documentation, and a .github directory with the workflow that runs the tests.

Fetching the script and making it executable

Setup starts with creating a webhook in the Discord text channel you want to post to, then downloading a copy of the script. Two download methods are given, one with curl and one with wget, and both pull the same file from the latest release.

bash
   # Using curl
   curl -o discord.sh https://github.com/fieu/discord.sh/releases/download/latest/discord.sh

   # Using wget
   wget -O discord.sh https://github.com/fieu/discord.sh/releases/download/latest/discord.sh

   # Make it executable
   chmod +x discord.sh

The final line matters, because the script is invoked as an executable rather than piped into bash. After that you point the script at a webhook endpoint, and the fourth step of the setup list is simply to go nuts, which is the project's own phrasing for the point at which the tool is ready to use. The release history shows the tool is still being worked on, with v2.0.1 in October 2025 adding stdin text input, v2.0.0 in February 2023 adding webhook modification support and v1.7.1 a few days before that for cleanup. The last push to the repository was on 2026-10-08, and the licence is GPL-3.0.

Three ways to supply the webhook URL

The script does not hardcode its endpoint, and the documentation gives three ways to supply it, in the order of priority the script takes. The first is passing the webhook URL as an argument with --webhook-url. The second is setting an environment variable called $DISCORD_WEBHOOK. The third is creating a file called .webhook in the same directory as discord.sh, containing only the webhook URL. That ordering is sensible for a script that runs in several contexts, since a command line argument overrides an environment variable which overrides a file, and each level is more or less persistent depending on how you run it. The .webhook file is the one to think about, because a file in the same directory as the script is a file you might commit. A webhook URL is a credential: anyone holding it can post to your channel, and Discord's own documentation for webhooks treats them as secrets to be kept private. The environment variable is the safest of the three for a shared repository, and the argument is the safest for a job that builds the URL at run time.

Sending text, and overriding the bot name and avatar

The simplest invocation passes a webhook URL and a text string, and the script posts it and exits.

bash
$ ./discord.sh --webhook-url="$WEBHOOK" --text "Hello, world!"

The next example adds a username and an avatar URL, which override the identity Discord shows for the webhook.

bash
$ ./discord.sh \
  --webhook-url="$WEBHOOK" \
  --username "NotificationBot" \
  --avatar "https://i.imgur.com/12jyR5Q.png" \
  --text "Hello, world!"

Two of these flags are the ones you will use most in practice. The username flag sets the name of the webhook bot, and the avatar flag sets its image, and the second example shows a script that reports as a different bot depending on which system it runs on. Both overrides are per message unless you use the modify flag described further down, which changes the webhook permanently. The text argument also accepts escape sequences, so a description can carry a newline inside it, which matters when you are formatting an embed body rather than a one-line alert.

Embeds: title, colour, author, fields and footer

The advanced example builds a full embed, and the option list it uses is the feature set of the tool. There is a title and a description, a color, a url that attaches to the title, an author with its own url and icon, an image and a thumbnail, fields given as semicolon separated name and value pairs, a footer with an icon, and a timestamp flag that lets Discord fill in the time. The color option takes two input syntaxes, a 0x prefix followed by a colour hex, or a decimal value, with the examples given as --color 0xFFFFFF and --color 16777215. The fields option is worth calling out because its syntax is unusual: each --field argument carries a name and a value separated by a semicolon, and the example passes two of them, "Author;ChaoticWeg" and "Author;fieu", which produce the two-column field layout Discord embeds support. The timestamp flag rounds out the set, and the whole thing is one command line assembled with backslash continuations, which is a nice demonstration that a Bash script can build a full Discord message without a JSON payload written by hand.

File uploads, and the embed and file conflict

Files can be attached, up to 8MB per Discord's own limits, and the example passes a file with a username and a text body.

bash
$ ./discord.sh \
  --webhook-url="$WEBHOOK" \
  --file README.md \
  --username "Notification Bot" \
  --text "Check out this README.md file!"

There is a constraint in the API itself that the script respects rather than papering over. Per the Discord webhook API, posts cannot contain embeds and file attachments at the same time, so discord.sh will bail out when asked to build a message with both. The documented best practice is to send the message with embeds first and then send the file, which is an awkward two call pattern for a CI job but is what the platform allows. The file path is passed as a normal argument, and the script works out the MIME type of the file to send, which is why the file command is listed as a dependency. That dependency list is more revealing than it first appears, because base64 and file exist for one feature: the webhook avatar modification path encodes the image and reads its type.

Piping content in, and permanently changing the webhook

Two options are worth their own treatment. The first is stdin, which lets you pipe content instead of using --text, and any --text argument is ignored when --stdin is used.

bash
$ echo "Hello from stdin!" | ./discord.sh --webhook-url="$WEBHOOK" --stdin

$ cat logfile.txt | ./discord.sh --webhook-url="$WEBHOOK" --stdin --username "Log Bot"

That is the mode that makes sense in a pipeline, where the content is whatever a previous command produced rather than a literal string. The second is --modify, which permanently changes the username and avatar of the webhook itself, so that all other webhook messages afterwards carry that identity by default. The option list for that mode is narrow, and the documentation is explicit that no other options may be passed, including those for sending messages.

bash
$ ./discord.sh \
  --modify \
  --username "NotifBot" \
  --avatar "https://i.imgur.com/12jyR5Q.png" 

The separation is sensible, since a modify call that also sent a message would make the resulting webhook state depend on the order of unrelated concerns. Note that the trailing space on the avatar line above is how the example appears in the documentation, and that modifying a webhook is not something you can undo through the script, so it is worth treating as a one-time setup step.

How the project is tested and linted

The Makefile shows a small project with a real test story. The all target runs the lint step first but only on Linux, checking the uname output, and then runs the tests. The test target invokes bats with --print-output-on-failure over the tests directory, so the suite is written with the Bash Automated Testing System, and the dependency list names bats for the tests. The lint target runs shellcheck over discord.sh, and it is careful about where shellcheck comes from. The Makefile looks for shellcheck on the path and, if it is not found, falls back to downloading a release into /tmp/shellcheck-latest/shellcheck, extracting it and making it executable, so a fresh checkout can lint itself without the contributor installing anything. The Makefile also carries a notify target used to post a result to a webhook, and a send.sh target that fetches a helper script from another repository. The consequence for a user of the tool is that the script is checked by a linter in CI and covered by a test suite, which is more assurance than a shell script of this size usually gets.

Where discord.sh is the wrong tool

The limits follow from the design rather than from missing features. The first is direction. Write-only means no reading, so a bot that needs to react to a message, answer a question or track a conversation cannot be built on this script at all. The second is Discord's own webhook model. A webhook posts into one channel and can be modified by anyone holding the URL, which makes it a poor fit for a two way bot and a good fit for alerting, and a script that stores a webhook URL in a file next to itself in a repository is a credential leak waiting to happen. The third is scale and control. There is no retry logic, no rate limit handling, no queue, no batching, and no structured output, so a CI system posting from many parallel jobs has to manage that itself. The fourth is dependency. The claim of only curl and jq at run time is true, but a shell script that posts to a third party API is still a network client, and the environment it runs in has to be able to reach Discord, which in a locked down CI runner may not be possible. And the fifth is formatting. The embed options are a good subset of what Discord supports, but the script is not a general Discord client, and for anything beyond a notification you would be writing the JSON by hand anyway. The influence from the Slack tool is visible in the flag design, which is a sensible precedent for anyone building the same thing for a different platform.

Editorial conclusion

Adopt discord.sh if you need to send a formatted message to a Discord channel from a shell script, a cron job or a CI step, and you would rather not add a Node or Python runtime to the machine. Do not adopt it if you need to read from Discord, since the project describes itself as write-only and there is no command to fetch messages. Verify two things before you wire it into a job: that the dependencies are present, since running it needs curl and jq even though the shipped script is a single file, and that your webhook URL is supplied in one of the three documented ways rather than hardcoded, since a webhook URL in a script committed to a repository is a credential in a repository. The practical first step is to download the release file, make it executable, set the webhook URL as an environment variable, and send one plain --text message before you try an embed.

Frequently asked questions

What is discord.sh?

It is a write-only command-line integration for Discord webhooks, written in 100% Bash script in less than 600 lines and influenced by an existing Slack command line tool. At run time it requires only curl and jq, with base64 and file used for webhook avatar modification.

How do I install discord.sh and make it executable?

Set up a webhook in the Discord text channel, then download the script from the latest release, either with curl -o discord.sh or wget -O discord.sh from the releases download URL, and run chmod +x discord.sh so it can be invoked as an executable.

How do I pass the Discord webhook URL to discord.sh?

Three ways, in priority order: pass it as an argument with --webhook-url, set the DISCORD_WEBHOOK environment variable, or create a .webhook file in the same directory as discord.sh containing only the webhook URL.

Can discord.sh send a file along with an embed?

No. Per the Discord webhook API, posts cannot contain embeds and file attachments together, so the script bails out when asked to build a message with both. The documented practice is to send the embed first and then send the file, and uploads are limited to 8MB by Discord.

What does the --modify flag do in discord.sh?

It permanently changes the username and avatar of the webhook, so later messages carry that identity by default. Only --username and --avatar may be passed with it, and no options for sending messages, and the change cannot be undone through the script.

How is the discord.sh script tested?

The Makefile runs the test suite with bats and --print-output-on-failure over the tests directory, and lints discord.sh with shellcheck, downloading a shellcheck release into /tmp when it is not on the path. The all target runs lint only on Linux and then the tests.

Official sources

  1. fieu/discord.sh on GitHub
  2. Issues
  3. License: GPL-3.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fieu-discord-sh.svg)](https://hysenlabs.com/projects/fieu-discord-sh)

Was this useful?