cowsay

Wrap text in a speech balloon and attach a cow to it

Updated 2026-08-20

cowsay reads text, wraps it in a speech balloon, and draws an ASCII figure underneath saying it. That is the whole program. It is here for the same reason wc is: it is a filter, it reads standard input, and it composes with everything else.

It is not part of a base Debian install, so start with sudo apt install cowsay.

Debian puts it in /usr/games rather than /usr/bin, and that directory is on your PATH at a login shell and on almost nothing else's, not cron's, not a systemd unit's, not a docker exec's. A script that works when you run it by hand and reports cowsay: not found from a cron job has hit that and nothing else, and the fix is the full path or a PATH= line at the top of the crontab. The same list is visible from the other end on sudo: command not found.

Input arrives one of two ways. Everything after the options is treated as the message, joined with spaces, so cowsay Hello world and cowsay 'Hello world' produce the same balloon. With no message argument it reads standard input instead, which is the form that matters: anything that prints can be piped into it.

cowsay reflows. It does not print your text, it re-wraps it: line breaks in the input are discarded, the whole thing is treated as one paragraph, and it is re-broken at 40 columns. Three lines in, one line out. -W changes the width and -n turns wrapping off entirely, but -n works only when the text arrives on standard input, and passing it a message argument prints the usage message instead of an error worth reading.

The figure comes from a cowfile. cowsay -l lists the ones installed, -f picks one, and $COWPATH is where it looks. A cowfile is not a picture: it is a small Perl program that cowsay executes, which is why -f pointed at a file from the internet deserves the same caution as any other downloaded script.

Sample files used on this page

Every example below was run against these files. Recreate them to follow along.

notes.txt three short lines, to show what wrapping does to them

Backup finished
14 files copied
0 errors

access.log 10 requests from 3 addresses, 7 of them 200s - the same sample log the text-processing pages use

203.0.113.5 - - [13/Aug/2026:09:12:01] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:09:12:03] "GET /style.css HTTP/1.1" 200 231
198.51.100.7 - - [13/Aug/2026:09:14:22] "GET /index.html HTTP/1.1" 200 512
198.51.100.7 - - [13/Aug/2026:09:14:25] "GET /missing.html HTTP/1.1" 404 162
203.0.113.5 - - [13/Aug/2026:09:15:47] "GET /index.html HTTP/1.1" 200 512
192.0.2.44 - - [13/Aug/2026:09:16:03] "POST /login HTTP/1.1" 302 0
192.0.2.44 - - [13/Aug/2026:09:16:04] "GET /dashboard HTTP/1.1" 200 4021
198.51.100.7 - - [13/Aug/2026:09:18:51] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:09:19:10] "GET /api/status HTTP/1.1" 500 89
192.0.2.44 - - [13/Aug/2026:09:20:33] "GET /dashboard HTTP/1.1" 200 4021

cows/server.cow a hand-written cowfile, using each of the three variables the format provides

$the_cow = <<"EOC";
     $thoughts
      $thoughts
        .-------------.
        |  $eyes  ------ |
        |  $tongue         |
        '-------------'
         \\___________/
EOC

downloaded.cow a cowfile that runs a command before drawing anything, standing in for one off the internet

print "(this line came from the cowfile, before any cow was drawn)\n";
$the_cow = <<"EOC";
      $thoughts
       $thoughts  (a perfectly ordinary cow)
EOC
51 outputs, collapsed by default

Saying something

Everything after the options is the message, joined with spaces. With no message at all, cowsay reads standard input instead - which is the form that makes it a filter rather than a toy.

Say a message

cowsay 'Hello, world'

The message goes in a balloon, sized to fit, with the default cow underneath.

Show output
 ______________
< Hello, world >
 --------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Skip the quotes for plain words

cowsay Hello world

Arguments are joined with a single space, so quoting only matters when the message contains something the shell would act on - a ;, a $, a *, or the run of spaces you wanted kept.

Show output
 _____________
< Hello world >
 -------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Think it instead of saying it

cowthink 'Was that meant to happen?'

A second command from the same package, with a thought balloon and a trail of bubbles instead of a pointer. Every option below works identically on both.

Show output
 ___________________________
( Was that meant to happen? )
 ---------------------------
        o   ^__^
         o  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Say nothing at all

cowsay ''

An empty message is not an error, and the balloon collapses to its minimum. Worth knowing when the thing you piped in turns out to have produced nothing.

Show output
 __
<  >
 --
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Find where Debian put it

dpkg -L cowsay | grep /usr/games

/usr/games, not /usr/bin. That directory is on a login shell's PATH and not on cron's, which is the single most common reason a working command stops working inside a job.

Show output
/usr/games
/usr/games/cowsay
/usr/games/cowthink

Confirm which one your shell will run

command -v cowsay

Prints nothing and exits non-zero if /usr/games is missing from your PATH, which is a faster answer than reading the error from whatever invoked it.

Show output
/usr/games/cowsay

Taking input from a pipe

With no message argument cowsay reads standard input, so anything that prints can feed it. Remember that it re-wraps what it receives: the line breaks in the input are gone unless you pass -n.

Pipe a message in

echo 'Backup finished' | cowsay

The form almost every real use takes. cowsay behaves like any other filter here - see pipes and redirection if the plumbing is the unfamiliar part.

Show output
 _________________
< Backup finished >
 -----------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Read a file

cowsay < notes.txt

Input from a file is re-broken at 40 columns like anything else, so the three lines become one paragraph. That is rarely what you want for a file.

Show output
 ___________________________________
/ Backup finished 14 files copied 0 \
\ errors                            /
 -----------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Keep the file's own line breaks

cowsay -n < notes.txt

-n turns wrapping off, so the input arrives as written. This is the fix for the previous example, and it works only because the text came in on standard input.

Show output
 _________________
/ Backup finished \
| 14 files copied |
\ 0 errors        /
 -----------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Feed it a here-string

cowsay <<< 'from a here-string'

A bash here-string is standard input without a pipeline or a subshell, which matters when the next thing you want to do is read $? from the command that produced the text.

Show output
 ____________________
< from a here-string >
 --------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Announce a count

wc -l < access.log | cowsay

< rather than a filename argument, so wc prints the number on its own instead of the number and the file name.

Show output
 ____
< 10 >
 ----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Announce the result of a whole pipeline

awk '{print $1}' access.log | sort -u | wc -l | cowsay

Distinct client addresses in the log. cowsay is the last stage and cares only that something arrived on standard input, so any pipeline at all can end this way.

Show output
 ___
< 3 >
 ---
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Report a match count with a themed cow

grep -c ' 200 ' access.log | cowsay -f apt

-f apt picks the cowfile the cowsay package ships for Debian's own package manager. Cowfiles are covered below.

Show output
 ___
< 7 >
 ---
       \ (__)
         (oo)
   /------\/
  / |    ||
 *  /\---/\
    ~~   ~~

Preserve the columns of a counted list

uniq -c < notes.txt | cowsay -n

uniq -c right-aligns its counts, and without -n that alignment is destroyed along with the line breaks. Any command whose output is a table needs -n here.

Show output
 _________________________
/       1 Backup finished \
|       1 14 files copied |
\       1 0 errors        /
 -------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Changing the face

Eight single-letter options set a preset expression, and two more set the eyes and tongue directly. They are mutually exclusive in practice: a preset always wins over -e, whichever order you write them in.

Assimilate the cow

cowsay -b 'Resistance is futile'

-b is "borg" mode, and sets the eyes to ==.

Show output
 ______________________
< Resistance is futile >
 ----------------------
        \   ^__^
         \  (==)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Report a failure

cowsay -d 'The build is dead'

-d is "dead": xx for eyes, and a lolling U tongue. The most useful of the presets, because it reads as failure at a glance in a script that also uses the default cow for success.

Show output
 ___________________
< The build is dead >
 -------------------
        \   ^__^
         \  (xx)\_______
            (__)\       )\/\
             U  ||----w |
                ||     ||

Show greed

cowsay -g 'Disk usage: 98%'

-g is "greedy", with dollar signs for eyes.

Show output
 _________________
< Disk usage: 98% >
 -----------------
        \   ^__^
         \  ($$)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Show paranoia

cowsay -p 'Who ran that as root?'

-p is "paranoid", with @@ for eyes.

Show output
 _______________________
< Who ran that as root? >
 -----------------------
        \   ^__^
         \  (@@)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Show a cow that has had enough

cowsay -s 'Deploy went fine, probably'

-s is "stoned", and like -d it sets the tongue as well as the eyes.

Show output
 ____________________________
< Deploy went fine, probably >
 ----------------------------
        \   ^__^
         \  (**)\_______
            (__)\       )\/\
             U  ||----w |
                ||     ||

Show tiredness

cowsay -t 'Third rebuild today'

-t is "tired", with half-closed -- eyes.

Show output
 _____________________
< Third rebuild today >
 ---------------------
        \   ^__^
         \  (--)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Show alertness

cowsay -w 'Tests passed'

-w is "wired", with wide OO eyes.

Show output
 ______________
< Tests passed >
 --------------
        \   ^__^
         \  (OO)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Show youth

cowsay -y 'First commit'

-y is "youthful", with small .. eyes.

Show output
 ______________
< First commit >
 --------------
        \   ^__^
         \  (..)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Set the eyes yourself

cowsay -e '^^' 'Custom eyes'

-e takes any two characters. Quote them: ^, $, * and @ all mean something to the shell first.

Show output
 _____________
< Custom eyes >
 -------------
        \   ^__^
         \  (^^)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Watch a longer string get truncated

cowsay -e 'XXXX' 'Only the first two count'

Exactly two characters are used and the rest are dropped silently, because the cowfile has a two-character slot to fill. There is no error and no warning.

Show output
 __________________________
< Only the first two count >
 --------------------------
        \   ^__^
         \  (XX)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Stick the tongue out

cowsay -T 'U ' 'Tongue out'

-T is the tongue, also two characters. The trailing space in 'U ' is deliberate: without it the second character comes from nowhere and the art shifts left.

Show output
 ____________
< Tongue out >
 ------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
             U  ||----w |
                ||     ||

Set both at once

cowsay -e '$$' -T 'vv' 'Both at once'

-e and -T are independent, so any combination works. Single quotes keep the shell away from the $$, which would otherwise expand to the shell's own process id.

Show output
 ______________
< Both at once >
 --------------
        \   ^__^
         \  ($$)\_______
            (__)\       )\/\
             vv ||----w |
                ||     ||

Find out which wins

cowsay -d -e '^^' 'Which one wins?'

The preset does, in either order. If a script sets a mode from a variable and also wants custom eyes, only one of the two can be honoured - so pick -e and set both characters yourself.

Show output
 _________________
< Which one wins? >
 -----------------
        \   ^__^
         \  (xx)\_______
            (__)\       )\/\
             U  ||----w |
                ||     ||

Combine a face with cowthink

cowthink -y -e 'oO' 'Thinking about it'

Same result - the preset overrides -e here too, and the only difference cowthink makes is the balloon and the bubbles.

Show output
 ___________________
( Thinking about it )
 -------------------
        o   ^__^
         o  (..)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Choosing a cow

The figure comes from a cowfile. -l lists what is installed, -f picks one by name, and $COWPATH is the colon-separated list of directories that -f searches - the same shape as PATH, and defaulting to /usr/share/cowsay/cows when it is unset.

List the cowfiles you have

cowsay -l

Names only, without the .cow extension, which is the form -f wants. The first line names the directory, so this doubles as a way to see what $COWPATH is currently resolving to.

Show output
Cow files in /usr/share/cowsay/cows:
apt bud-frogs bunny calvin cheese cock cower daemon default dragon
dragon-and-cow duck elephant elephant-in-snake eyes flaming-sheep fox
ghostbusters gnu hellokitty kangaroo kiss koala kosh luke-koala
mech-and-cow milk moofasa moose pony pony-smaller ren sheep skeleton
snowman stegosaurus stimpy suse three-eyes turkey turtle tux unipony
unipony-smaller vader vader-koala www

Count them

cowsay -l | tail -n +2 | wc -w

tail -n +2 drops the "Cow files in ..." heading, leaving only names for wc -w to count. Debian's cowsay package ships 47.

Show output
47

Pick one by name

cowsay -f tux 'Debian'

-f takes the name as -l printed it. The extension is optional - -f tux and -f tux.cow both work.

Show output
 ________
< Debian >
 --------
   \
    \
        .--.
       |o_o |
       |:_/ |
      //   \ \
     (|     | )
    /'\_   _/`\
    \___)=(___/

Use the cow the package manager gets

cowsay -f apt 'Reading package lists...'

Debian ships its own apt logo as a cowfile. It is the smallest of them, which makes it a good default when the output has to sit inside something else.

Show output
 __________________________
< Reading package lists... >
 --------------------------
       \ (__)
         (oo)
   /------\/
  / |    ||
 *  /\---/\
    ~~   ~~

Use a much larger one

cowsay -f stegosaurus 'Legacy code'

Cowfiles vary enormously in size. Check one before putting it in a login banner or a CI log, because nothing constrains the height and the balloon sits above whatever it is.

Show output
 _____________
< Legacy code >
 -------------
\                             .       .
 \                           / `.   .' "
  \                  .---.  <    > <    >  .---.
   \                 |    \  \ - ~ ~ - /  /    |
         _____          ..-~             ~-..-~
        |     |   \~~~\.'                    `./~~~/
       ---------   \__/                        \__/
      .'  O    \     /               /       \  "
     (_____,    `._.'               |         }  \/~~~/
      `----.          /       }     |        /    \__/
            `-.      |       /      |       /      `. ,~~|
                ~-.__|      /_ - ~ ^|      /- _      `..-'
                     |     /        |     /     ~-.     `-. _  _  _
                     |_____|        |_____|         ~ - . _ _ _ _ _>

See what a wrong name reports

cowsay -f nosuch 'hello'

A clear error and nothing else - no balloon, no fallback to the default cow. Anything less obvious than this means the file was found and something inside it went wrong.

Show output
cowsay: Could not find nosuch cowfile!

Use a cowfile by path

cowsay -f ./cows/server.cow 'Backup finished'

Any -f value containing a / is treated as a path and $COWPATH is skipped entirely. The leading ./ is not optional - see the last section for what happens without it.

Show output
 _________________
< Backup finished >
 -----------------
     \
      \
        .-------------.
        |  oo  ------ |
        |             |
        '-------------'
         \___________/

Point COWPATH at your own directory

COWPATH=$PWD/cows cowsay -f server 'Backup finished'

With $COWPATH set, -f takes a bare name again and finds your cowfile the same way it finds a shipped one. Set it in ~/.bashrc to make it permanent.

Show output
 _________________
< Backup finished >
 -----------------
     \
      \
        .-------------.
        |  oo  ------ |
        |             |
        '-------------'
         \___________/

List what a custom COWPATH offers

COWPATH=$PWD/cows cowsay -l | tail -1

-l walks every directory in $COWPATH and prints a heading for each. tail -1 here keeps the output to the names, since the headings are absolute paths specific to your machine.

Show output
server

Think with a chosen cow

cowthink -f tux 'Compiling'

-f works on cowthink unchanged. Only the balloon and the connector differ, so every cowfile has a thinking form for free.

Show output
 ___________
( Compiling )
 -----------
   o
    o
        .--.
       |o_o |
       |:_/ |
      //   \ \
     (|     | )
    /'\_   _/`\
    \___)=(___/

Pick a different cow every time

cowsay -f "$(cowsay -l | tail -n +2 | tr ' ' '\n' | shuf -n 1)" 'Surprise'

tr turns the space-separated list into one name per line so shuf -n 1 can choose one. Output differs every run, which is the point, so nothing is shown here.

Writing your own cowfile

A cowfile sets one variable, $the_cow, to the art. It is Perl, and the conventional form is an interpolating heredoc so that three variables can be substituted into the drawing: $eyes and $tongue (two characters each, from -e and -T) and $thoughts, which is \ for cowsay and o for cowthink.

Read the shipped default first

cat /usr/share/cowsay/cows/default.cow

The shipped default is eight lines and uses every convention the format has. Note that the backslashes in the art are doubled - the heredoc interpolates, so a single one would be consumed before the art ever reached the screen.

Show output
$the_cow = <<"EOC";
        $thoughts   ^__^
         $thoughts  ($eyes)\\_______
            (__)\\       )\\/\\
             $tongue ||----w |
                ||     ||
EOC

Read one you wrote

cat cows/server.cow

The same shape. $tongue is two spaces when -T is not given, which is why the line holding it is padded to look empty rather than short.

Show output
$the_cow = <<"EOC";
     $thoughts
      $thoughts
        .-------------.
        |  $eyes  ------ |
        |  $tongue         |
        '-------------'
         \\___________/
EOC

Watch the variables fill in

cowsay -f ./cows/server.cow -d -T 'U ' 'Disk full'

-d writes into $eyes and -T into $tongue, in a cowfile that never mentions either option. Any cowfile that uses the variables gets every option on this page for nothing.

Show output
 ___________
< Disk full >
 -----------
     \
      \
        .-------------.
        |  xx  ------ |
        |  U          |
        '-------------'
         \___________/

Install it where cowsay looks

mkdir -p ~/.cows && cp cows/server.cow ~/.cows/

Nothing about ~/.cows is special to cowsay; it is just a directory you are about to name in $COWPATH. Keeping your cowfiles out of /usr/share means a package upgrade cannot remove them.

Make COWPATH permanent

echo 'export COWPATH=$HOME/.cows:/usr/share/cowsay/cows' >> ~/.bashrc

Your own directory comes first, so your cowfiles win a name clash and every shipped one still resolves. Single quotes so $HOME is written literally and expanded when the shell reads the file.

Controlling the wrapping

cowsay re-wraps everything it is given at 40 columns by default, joining the input into one paragraph first. -W changes the width; -n switches wrapping off, and works only on text arriving via standard input.

See the default width

cowsay 'The quick brown fox jumps over the lazy dog and keeps on running'

40 columns, which is narrow enough that most command output wraps. The balloon changes shape once it needs more than one line - the < > becomes a / \ and \ / frame.

Show output
 _________________________________________
/ The quick brown fox jumps over the lazy \
\ dog and keeps on running                /
 -----------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Wrap at a narrower width

cowsay -W 20 'The quick brown fox jumps over the lazy dog'

Middle lines of a multi-line balloon are framed with | on both sides, so the shape tells you at a glance how many lines there are.

Show output
 _____________________
/ The quick brown fox \
| jumps over the lazy |
\ dog                 /
 ---------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Wrap at a wider one

cowsay -W 70 'The quick brown fox jumps over the lazy dog and keeps on running'

Give it more than the text needs and the balloon goes back to a single line. -W is a maximum, not a fixed width.

Show output
 __________________________________________________________________
< The quick brown fox jumps over the lazy dog and keeps on running >
 ------------------------------------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Watch line breaks disappear

printf 'one\ntwo\nthree\n' | cowsay

The input's line breaks are gone, which is behind almost every "why does my output look wrong" question about cowsay, and it applies to anything piped in.

Show output
 _______________
< one two three >
 ---------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Keep them

printf 'one\ntwo\nthree\n' | cowsay -n

-n passes the text through as it arrived. Nothing is wrapped, so a line longer than your terminal will still run off the edge - -n and -W are alternatives, not partners.

Show output
 _______
/ one   \
| two   |
\ three /
 -------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Using it in a script

The two things worth remembering when this moves out of your terminal: /usr/games is not on the PATH of anything scheduled, and cowsay writes to standard output like everything else, so it redirects and pipes normally.

Report success or failure differently

if make -s; then cowsay 'Build finished'; else cowsay -d 'Build failed'; fi

The dead cow for the failure branch is legible from across a room, which is the entire argument for doing this at the end of a long job. No output shown because it depends on your build.

Wrap it in a shell function

notify() { cowsay -W 60 "$*"; }

"$*" joins the arguments into one string, so notify deploy finished works without quotes at the call site. Put it in ~/.bashrc alongside your other functions.

Announce it from a git hook

printf '#!/bin/sh\ncowsay "Pushed to $(git branch --show-current)"\n' > .git/hooks/pre-push

Hooks run with your environment, so /usr/games is normally on the PATH here. Remember chmod +x - a hook that is not executable is skipped silently.

Give cron the full path

0 9 * * 1 /usr/games/cowsay 'Weekly report is due' | mail -s Reminder you

The reliable form inside a crontab, because cron's PATH is /usr/bin:/bin and nothing else. Setting PATH= at the top of the crontab works too.

See what cron would have seen

env -i PATH=/usr/bin:/bin sh -c 'cowsay Scheduled backup done'

env -i clears the environment so you can reproduce a scheduled job's PATH deliberately, rather than finding out at 09:00. The error comes from dash, which is what sh is on Debian.

Show output
sh: 1: cowsay: not found

Things that go wrong

Three failures that look like something else. The last is a security note rather than a cosmetic one, and it is the reason a cowfile from a stranger deserves the same reading as any other downloaded script.

Watch colour codes break the balloon

printf '\033[31mDeploy failed\033[0m on host deb1\n' | cowsay | cat -v

cowsay counts the escape sequences as characters when it sizes and pads the balloon, so the frame no longer lines up with the visible text. cat -v is only here to make the escapes printable on this page; strip colour before piping in, or accept a ragged edge.

Show output
 ____________________________
< ^[[31mDeploy failed^[[0m on host deb1 >
 ----------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Forget the ./ on a relative cowfile

cowsay -f cows/server.cow 'Backup finished'

A path with a / in it is handed to Perl's do, which will not read a relative path that does not start with ./. The balloon is drawn and the cow is not, and the error names Perl rather than cowsay.

Show output
do "cows/server.cow" failed, '.' is no longer in @INC; did you mean do "./cows/server.cow"? at /usr/games/cowsay line 196.
 _________________
< Backup finished >
 -----------------

Pass -n a message argument

cowsay -n 'why is this the usage message'

-n applies only to standard input, and giving it a message is treated as a usage error rather than reported as one. Nothing tells you which option it objected to.

Show output
cow{say,think} version 3.03, (c) 1999 Tony Monroe
Usage: cowsay [-bdgpstwy] [-h] [-e eyes] [-f cowfile]
          [-l] [-n] [-T tongue] [-W wrapcolumn] [message]

Run a cowfile you did not write

cowsay -f ./downloaded.cow 'Nice cow'

A cowfile is executed, not parsed - cowsay loads it with Perl's do, so anything in the file runs with your privileges before a single character is drawn. Read a downloaded cowfile first; a legitimate one is a $the_cow assignment, a heredoc of art, and nothing else.

Show output
(this line came from the cowfile, before any cow was drawn)
 __________
< Nice cow >
 ----------
      \
       \  (a perfectly ordinary cow)