October 15, 2026
Practicing Piping: A Journey Through Linux I/O Redirection
A hands-on walkthrough of pwn.college’s “Practicing Piping” module — the mistakes, the eureka moments, and everything I learned about…

By David Wairimu
7 min read
A hands-on walkthrough of pwn.college's "Practicing Piping" module — the mistakes, the eureka moments, and everything I learned about stdin, stdout, and stderr.
Introduction
When I started the Practicing Piping module in pwn.college's Linux Luminarium, I thought I already understood the command line. I could run commands, read files, and move around the filesystem. What I didn't realize was how much was happening underneath every command I typed.
This module pulled back the curtain on one of the most fundamental concepts in Linux: the three standard communication channels every process gets.
- stdin (FD 0) — Standard Input: where a process reads from
- stdout (FD 1) — Standard Output: where a process writes normal data
- stderr (FD 2) — Standard Error: where a process writes error messages
The module promised to teach me how to redirect, chain, block, and otherwise mess with these channels. It delivered — and then some. What follows is my honest account of the journey: the walls I hit, the misconceptions I had, and the moment each level finally clicked.
Level 1: Redirecting Output
The first challenge was straightforward: use > to redirect echo's output into a file named COLLEGE.
bash
echo PWN > COLLEGEecho PWN > COLLEGEMy mistake: I created college instead of COLLEGE. Linux filenames are case-sensitive, and I'd gotten too comfortable assuming things would "just work." The checker rejected it instantly.
Lesson learned: In Linux, college ≠ COLLEGE ≠ College. Every character matters, always. This would come back to bite me later.
Level 2: Redirecting More Output
Now I had to redirect /challenge/run's stdout to a file called myflag — but the challenge's feedback came over stderr, so I'd still see messages even with stdout redirected.
My mistake: I ran /challenge/run bare, expecting the checker to somehow know what I meant. It didn't.
bash
/challenge/run > myflag
cat myflag/challenge/run > myflag
cat myflagLesson learned: The three streams are separate. Redirecting stdout doesn't touch stderr, and vice versa. This separation is the foundation for everything that follows.
Level 3: Appending Output
Here's where the > vs >> distinction mattered. The challenge wrote the first half of the flag directly to a file and the second half to stdout. If I used >, the second write would truncate the file and wipe out the first half.
bash
/challenge/run >> /home/hacker/the-flag/challenge/run >> /home/hacker/the-flagLesson learned:
OperatorBehavior>Truncate — create or wipe>>Append — create or add
The ASCII art in the output file was a nice touch — the flag was literally stitched together from two writes.
Level 4: Redirecting Errors
This one introduced file descriptors as numbers. > is shorthand for 1>, and 2> redirects stderr. I needed to send stdout to myflag and stderr to instructions — in a single command.
My mistakes:
- Typed
myflagg(typo — the checker caught it) - Redirected one stream per command instead of both together
- Ran a placeholder command (
some_command) from the tutorial, which naturally failed
bash
/challenge/run > myflag 2> instructions/challenge/run > myflag 2> instructionsLesson learned: Redirections apply to one command invocation. To send both streams somewhere different, combine them on the same line.
I also discovered that when cat fails, its error goes to stderr — which is why I saw cat: 2: No such file or directory when I accidentally typed cat myflag 2 (bash interpreted 2 as a second filename).
Level 5: Redirecting Input
< redirects a file's contents into a program's stdin. This level required /challenge/run < PWN where PWN contained the value COLLEGE.
My mistakes:
- Redirected a lowercase
pwnfile (case sensitivity again!) - Redirected
PWNbefore creating it — bash errored with "No such file or directory"
bash
echo COLLEGE > PWN
/challenge/run < PWNecho COLLEGE > PWN
/challenge/run < PWNLesson learned: Redirections are set up before the command runs. If the source file doesn't exist, bash fails immediately.
Level 6: Grepping Stored Results
The challenge: redirect /challenge/run's output (100,000 lines) to /tmp/data.txt, then grep for the flag.
My mistakes:
- Tried to execute
/tmp/data.txtas a program → "Permission denied" - Typo'd
data.txinstead ofdata.txt - Forgot the full path when
cat-ing
bash
/challenge/run > /tmp/data.txt
grep pwn.college /tmp/data.txt/challenge/run > /tmp/data.txt
grep pwn.college /tmp/data.txtLesson learned: This is the classic CTF workflow — dump, then filter. Two steps, done cleanly.
Level 7: Grepping Live Output
The pipe (|) eliminates the middleman. Instead of saving to a file and grepping, pipe directly.
My mistakes:
- Ran
/challenge/runbare — checker wanted a pipe on stdout - Ran
grep /challenge/run— searched for the string "challenge/run" instead of searching its output. I had the direction backwards.
bash
/challenge/run | grep pwn.college/challenge/run | grep pwn.collegeLesson learned: | connects stdout of the left to stdin of the right. No intermediate file needed. This is the heart of the Unix philosophy — small tools, connected.
Level 8: Grepping Errors
Now the output was on stderr, and | only pipes stdout. There's no 2| operator. The solution: merge stderr into stdout with 2>&1, then pipe.
My mistakes:
/challenge/run | grep ...— only stdout was piped, so grep saw nothing2>& | grep ...— missing the1. The & signals a file descriptor, and the target must follow.
bash
/challenge/run 2>&1 | grep pwn.college/challenge/run 2>&1 | grep pwn.collegeLesson learned: 2>&1 means "redirect FD 2 to whatever FD 1 points to." The & is essential — without it, 2> 1 would write to a file literally named 1. And order matters: the merge must happen on the left side of the pipe.
bash
command 2>&1 | grep pattern ✅ correct
command | grep pattern 2>&1 ❌ 2>&1 applies to grepcommand 2>&1 | grep pattern ✅ correct
command | grep pattern 2>&1 ❌ 2>&1 applies to grepLevel 9: Filtering with grep -v
Invert match: show lines that don't match. The challenge mixed 1000+ decoys (all containing "DECOY") with the real flag.
bash
/challenge/run | grep -v DECOY/challenge/run | grep -v DECOYLesson learned: Sometimes the easiest way to keep what you want is to throw away what you don't. grep -v is the right tool when the bad data has an obvious marker.
Level 10: Filtering with sed
Here the garbage was inside each line — "FAKEFLAG" between every character. grep can't help; it's all-or-nothing per line. Enter sed.
My mistake: I ran grep sed "s/oldword/newword/g", which told grep to search for the pattern "sed" inside nonexistent files. grep died immediately, and /challenge/run flooded the terminal with Broken pipe errors because nothing was reading.
bash
/challenge/run | sed "s/FAKEFLAG//g"/challenge/run | sed "s/FAKEFLAG//g"Lesson learned:
ToolPurposegrepSelect whole linessedEdit within lines
And that Broken pipe flood? It's what happens when a reader dies mid-pipeline. The writer's write() calls get EPIPE. Harmless, but a good lesson in pipeline mechanics.
Level 11: Duplicating Piped Data with tee
tee is the T-splitter — one stream in, multiple out. The challenge required tee between two commands, not at the end.
My mistakes:
pwn | tee /tmp/intercepted—teewas last in the pipeline; the consumer was missingpwn | tee— no filenamecommand_1 | command_2— placeholders, not real commandspwn --secret <the-value-you-saw> | ...— typed the angle brackets literally. Bash saw < as input redirection and threw a syntax error.
bash
/challenge/pwn | tee /tmp/intercepted | /challenge/college
cat /tmp/intercepted
# → SECRET_ARG should be "4lDZ_ZZm"
/challenge/pwn --secret 4lDZ_ZZm | /challenge/college/challenge/pwn | tee /tmp/intercepted | /challenge/college
cat /tmp/intercepted
# → SECRET_ARG should be "4lDZ_ZZm"
/challenge/pwn --secret 4lDZ_ZZm | /challenge/collegeLesson learned: The debug pattern:
text
producer | tee logfile | consumerproducer | tee logfile | consumertee sits in the middle, captures a snapshot, and passes data through. This is the real-world use of tee.
Also: angle brackets in tutorials are placeholders — replace them, don't type them.
Level 12: Process Substitution for Input
Process substitution <(...) lets you use a command's output as a file argument. Perfect for diff, which wants two filenames.
bash
diff <(/challenge/print_decoys) <(/challenge/print_decoys_and_flag)diff <(/challenge/print_decoys) <(/challenge/print_decoys_and_flag)Output:
text
4a5
> pwn.college{soANWbtYWsSIUBu0Yb3QzdcUWAD.0lNwMDOxwyN0czMzIzW}4a5
> pwn.college{soANWbtYWsSIUBu0Yb3QzdcUWAD.0lNwMDOxwyN0czMzIzW}Lesson learned: Bash replaces <(...) with a path like /dev/fd/63 — a named pipe. Any tool that requires file arguments can now consume live command output. No temp files, no cleanup.
Reading the diff: 4a5 = "after line 4 in file 1, add line 5 from file 2." The single added line is the flag.
Level 13: Writing to Multiple Programs
Process substitution also works in write mode: >(command) hooks a named pipe to a command's stdin.
My mistakes:
/challenge/hackbare — no redirection/challenge/hack | tee /challenge/planet—teeinterpreted/challenge/planetas a filename, not a command. Permission denied./challenge/hack | tee(/challenge/the) >(/challenge/planet)— bash syntax error;teedoesn't take parenthesized arguments
bash
/challenge/hack | tee >(/challenge/the) >(/challenge/planet)/challenge/hack | tee >(/challenge/the) >(/challenge/planet)Lesson learned: The shell expands >(...) before tee runs. tee only sees /dev/fd/63 and /dev/fd/64. The syntax is "magic" at the shell level, invisible to the command.
The fan-out pattern:
text
producer | tee >(cmd1) >(cmd2)producer | tee >(cmd1) >(cmd2)Level 14: Split-Piping stderr and stdout
The final boss: route stdout to one program and stderr to another — unmixed. 2>&1 would merge them, which is exactly what we don't want.
My mistake: /challenge/hack | tee(/challenge/the) >(/challenge/planet) — same tee( syntax error, plus tee would merge streams anyway.
bash
/challenge/hack 2> >(/challenge/the) | /challenge/planet/challenge/hack 2> >(/challenge/the) | /challenge/planetLesson learned: The trick is realizing tee is the wrong tool. You don't want to duplicate a stream — you want to route two streams separately.
- | pipes only stdout →
/challenge/planet 2> >(...)routes stderr →/challenge/the
Bonus (no pipe):
bash
/challenge/hack > >(/challenge/planet) 2> >(/challenge/the)/challenge/hack > >(/challenge/planet) 2> >(/challenge/the)Symmetric and pipe-free — both streams routed with > >(...). The challenge's message: "a redirection technique that even experts struggle with!"
Critical gotcha: The space in 2> >(...) is required. Gluing them as 2>>(...) mis-parses.
Level 15: Named Pipes (FIFOs)
The final challenge: create a FIFO and redirect /challenge/run's stdout into it.
My mistakes:
echo pwn > myfifo— hung. No reader. I hit^C. This is blocking behavior in action.- Tried to execute
/tmp/flag_fifoas a program /challenge/runbare — stdout didn't point to a FIFOcat flag_fifo— wrong path (FIFO was at/tmp/flag_fifo)
The solution — two terminals:
bash
# Terminal 1
cat /tmp/flag_fifo
# Terminal 2
/challenge/run > /tmp/flag_fifo# Terminal 1
cat /tmp/flag_fifo
# Terminal 2
/challenge/run > /tmp/flag_fifoSingle-terminal alternative:
bash
cat /tmp/flag_fifo &
/challenge/run > /tmp/flag_fifo
waitcat /tmp/flag_fifo &
/challenge/run > /tmp/flag_fifo
waitLesson learned: FIFOs block until both ends connect. This is a feature, not a bug — it provides automatic synchronization. Writers wait for readers, readers wait for writers.
FeatureRegular fileFIFOStorageOn diskIn memoryPersistenceAfter readGone after readSyncManual orderingAutomatic blockingMulti-reader/writerNoYes
The Complete Toolkit
After this module, my mental model of Linux I/O is transformed:
OperatorPurpose>stdout → file (truncate)>>stdout → file (append)2>stderr → file<file → stdin|stdout → stdin of next command2>&1merge stderr into stdout<()command output → file (read)>()file → command input (write)mkfifocreate a persistent named pipe
And the composed patterns:
bash
# Save and inspect
cmd > file; grep pattern file
# Grep live output
cmd | grep pattern
# Grep live errors
cmd 2>&1 | grep pattern
# Debug a pipeline
producer | tee logfile | consumer
# Fan-out to multiple commands
producer | tee >(cmd1) >(cmd2)
# Split streams
cmd 2> >(err_consumer) | out_consumer# Save and inspect
cmd > file; grep pattern file
# Grep live output
cmd | grep pattern
# Grep live errors
cmd 2>&1 | grep pattern
# Debug a pipeline
producer | tee logfile | consumer
# Fan-out to multiple commands
producer | tee >(cmd1) >(cmd2)
# Split streams
cmd 2> >(err_consumer) | out_consumerThe Biggest Lessons
- Case sensitivity is real.
college,COLLEGE, andCollegeare three different things. I tripped on this twice. - The three streams are independent. Redirecting one doesn't affect the others — that's the whole point.
-
truncates, >> appends. Choose deliberately.
- Order matters.
2>&1 |must come before the pipe, on the left side. - Angle brackets in tutorials are placeholders. Never type them literally — the shell will interpret them as redirection.
- The shell expands things before commands run. >(...) becomes
/dev/fd/63beforeteeever sees it. - Blocking is a feature. FIFOs hang until both ends connect, and that gives you free synchronization.
- Don't overuse process substitution. The challenge itself warns: "it's a very specialized tool; don't use it for everything!" Pipes are the everyday tool.
Final Thoughts
The Practicing Piping module took me from "I can run commands" to "I understand how every process on this system talks to the world." That's a fundamental shift in how I see the command line.
Every mistake I made was instructive. The case-sensitivity errors taught me precision. The blocking FIFO taught me patience. The Broken pipe flood taught me what happens when pipelines break. The 2>&1 | ordering taught me that shell syntax isn't forgiving — but it is logical once you see the pattern.
If you're working through pwn.college's Linux Luminarium, take your time with this module. Don't rush past the errors — they're the best teachers. And when you finally split stderr and stdout into two different programs on a single command line, take a moment to appreciate it. You've just learned something many people never bother to understand.
Onward to the next module. 🚀
Written as part of my journey through pwn.college's Linux Luminarium. If you found this helpful, follow along for more write-ups as I work through the rest of the curriculum.