The super tip for the fg command: when Ctrl+Z saves your terminal session
Published on 18 April 2026
- The problem: a busy terminal
- The three essential commands
- Scenario 1: The long build
- Scenario 2: The Forgotten Editor
- Scenario 3: The opencode process suspended
- Scenario 4: Managing several jobs simultaneously
- Differences between fg, bg, Ctrl+Z, Ctrl+C and &
- The complete life cycle of a job
- Advanced tips
- Common pitfalls and mistakes
- Summary diagram
- Why fg is underestimated
- Technical memorandum: the signals
- Conclusion
- References
You throw`./gradlew build`, the terminal is blocked for 3 minutes, and you need to do something else while waiting? No need to open a new tab.`Ctrl+Z`suspends the process,`fg`It brings it back exactly where you left it. It’s the most underrated Unix tip in a developer’s daily routine.
- knock
-
[]
The problem: a busy terminal
Every developer has experienced this situation:
$ ./gradlew build
> Building...
The terminal is frozen. You can no longer type any command. And you need to consult a file, run a`git status`, or check a busy port.
@startuml skinparam backgroundColor #FEFEFE actor Développeur as dev participant "Terminal" as term participant "Gradle\n( process )" as gradle dev -> term : ./gradlew build term -> gradle : Lance le build gradle -> term : Occupe le terminal\n(pas de prompt) dev -> term : veut taper une commande term --> dev : ❌ Terminal bloqué note right of dev Le développeur est paralysé pendant toute la durée du build end note @enduml
Most developers open a new terminal. But there is a more elegant, faster, and native solution: job management.
The three essential commands
Ctrl+Z — Suspend (SIGTSTP)
`Ctrl+Z`sends the signal`SIGTSTP`to the foreground process. The process issuspended— not killed — and the terminal regains its prompt.
$ ./gradlew build
> Building...
^Z
[1]+ Stoppé ./gradlew build
$
The number in brackets`[1]`is thejob number. Le `+`indicates the default job.
fg — Bring to foreground
`fg`brings the suspended job to the foreground. The process resumes exactly where it left off.
$ fg
./gradlew build
> Building... (reprend où il en était)
With a specific job number:
$ fg %2
bg — Resume in the background
`bg`Resume the process without blocking the terminal. Ideal for long builds.
$ bg
[1]+ ./gradlew build &
$
Le `&`at the end means that the process is running in the background.
jobs — List the shell’s jobs
$ jobs
[1]- Stoppé vim README.md
[2]+ En cours ./gradlew build
@startuml skinparam backgroundColor #FEFEFE skinparam defaultFontName sans-serif state "Foreground\n(terminal locked)" as fg #LightCoral state "Suspended\n(Ctrl+Z)" as stopped #LightYellow state "Background\n(bg)" as bg_ #LightGreen [*] --> fg : Lance une commande fg --> stopped : Ctrl+Z\n(SIGTSTP) stopped --> fg : fg\nreprend en avant-plan stopped --> bg_ : bg\nreprend en arrière-plan bg_ --> fg : fg\nramène en avant-plan fg --> [*] : Commande terminée note right of stopped Le processus est figé mais _pas_ tué end note note left of bg_ Le terminal est libre le build continue end note @enduml
Scenario 1: The long build
This is the most common use case. You start a Gradle build, the terminal is blocked, and you need to do something else.
@startuml skinparam backgroundColor #FEFEFE autonumber actor Développeur as dev participant "Terminal" as term participant "Gradle" as gradle database "State\nprocess" as state dev -> term : ./gradlew build term -> gradle : Lance le build gradle -> state : En cours (FG) dev -> term : Ctrl+Z term -> state : Suspendu (job [1]) term --> dev : Prompt libre $ dev -> term : git status term --> dev : working tree clean dev -> term : fg term -> state : Reprend (FG) gradle --> dev : Build réussi ✓ note right of state Le processus Gradle n'a jamais été tué Il reprend exactement où il s'était arrêté end note @enduml
Step by step
$ ./gradlew build
> Building...
^Z
[1]+ Stoppé ./gradlew build
$ git status
On branch main
nothing to commit, working tree clean
$ fg
./gradlew build
> Building...
BUILD SUCCESSFUL in 2m 34s
Scenario 2: The Forgotten Editor
You are in`vim`, you need to consult another file without leaving`vim`.
# Dans vim, en mode commande :
:shell
$ cat /path/to/other/file.txt
$ exit
# Retour dans vim
But it’s heavy. With`Ctrl+Z`, it is immediate :
# Dans vim, n'importe quel moment :
Ctrl+Z
[1]+ Stoppé vim README.md
$ cat /path/to/other/file.txt
# ... lire le fichier ...
$ fg
vim README.md
# Retour exact là où on en était, curseur en place
@startuml skinparam backgroundColor #FEFEFE autonumber actor Développeur as dev participant "vim" as vim participant "Shell" as shell dev -> vim : édition en cours dev -> vim : Ctrl+Z vim -> shell : Suspendu (job [1]) dev -> shell : cat autre_fichier.txt shell --> dev : contenu du fichier dev -> shell : fg shell -> vim : Reprend (job [1]) vim --> dev : Curseur exactement\nà la même position note right of vim vim restaure intégralement l'état : position du curseur, buffer, undo stack end note @enduml
vim`parfaitement restores its state after a`fg: cursor position, buffer content, undo history — everything is preserved.
|
Scenario 3: The opencode process suspended
This is the scenario that motivated this article. You use`opencode`(CLI tool for running an LLM on your code), and the process is suspended.
@startuml skinparam backgroundColor #FEFEFE skinparam defaultFontName sans-serif skinparam componentStyle rectangle actor Développeur as dev participant "opencode\n(LLM agent)" as agent participant "Shell" as shell participant "git" as git dev -> agent : Session active agent -> agent : Tour de parole\ndu LLM en cours note over agent Ctrl+Z arrive en plein milieu d'une analyse de code end note dev -> agent : Ctrl+Z agent -> shell : Suspendu (job [1]) dev -> git : git diff git --> dev : changements visibles dev -> shell : fg shell -> agent : Reprend (job [1]) agent --> dev : Reprend exactement\nl'analyse en cours note right of agent Aucune perte de contexte Le LLM reprend sa réponse exactement où il en était end note @enduml
$ opencode
# ... session en cours, LLM analyse le code ...
^Z
[1]+ Stoppé opencode
$ git diff HEAD~1
# vérifier les changements récents
$ fg
opencode
# Le LLM reprend là où il s'était arrêté
The advantage is twofold:
-
No loss of context: the LLM session, conversation history, open files — everything is preserved
-
No restart: no need to restart opencode, reload the context, re-explain the problem to the LLM
Scenario 4: Managing several jobs simultaneously
fg et `bg`They accept a job number to target a specific process when multiple are suspended.
$ vim config.yml
^Z
[1]+ Stoppé vim config.yml
$ ./gradlew test
^Z
[2]+ Stoppé ./gradlew test
$ htop
^Z
[3]+ Stoppé htop
$ jobs
[1] Stoppé vim config.yml
[2]- Stoppé ./gradlew test
[3]+ Stoppé htop
$ fg %2
./gradlew test
# Le build reprend en avant-plan
$ bg %3
[3] htop &
# htop reprend en arrière-plan
@startuml
skinparam backgroundColor #FEFEFE
rectangle "Suspended jobs" as jobs #LightYellow {
card "[1] vim config.yml" as j1
card "[2] ./gradlew test" as j2
card "[3] htop" as j3
}
rectangle "Actions" as actions #LightGreen {
card "fg %2\n→ foreground" as a1
card "bg %3\n→ background" as a2
card "fg %1\n→ foreground" as a3
}
jobs --> actions : fg/bg + numéro de job
actions --> j2 : Reprend
actions --> j3 : En arrière-plan
note bottom of jobs
Chaque job conserve
son état exact
entre les suspend/resume
end note
@enduml
Summary of job commands
| Order | Effect | Example |
|---|---|---|
|
Suspend the process in the foreground |
Sending of`SIGTSTP` |
|
Resume the default job in the foreground |
|
|
Resume job n in the foreground |
|
|
Resume the default job in the background |
|
|
Resume job n in the background |
|
|
List the jobs of the current shell |
|
|
Finish the job n |
|
Differences between fg, bg, Ctrl+Z, Ctrl+C and &
Many developers confuse these mechanisms. Here is a decisive clarification.
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontName sans-serif
rectangle "**Ctrl+Z**\nSIGTSTP" as ctrlz #LightYellow {
card "Suspend the process\n(it is still in memory)\nCan be used with fg/bg" as c1
}
rectangle "**Ctrl+C**\nSIGINT" as ctrlc #LightCoral {
card "Kill the process\n(no return possible)\nLoss of all state" as c2
}
rectangle "**& **\n(background)" as ampersand #LightGreen {
card "Launch directly\nin the background\nTerminal free immediately" as c3
}
rectangle "**fg**\n(foreground)" as fg_ #SkyBlue {
card "Bring a suspended job\nto the foreground\nTerminal busy" as c4
}
rectangle "**bg**\n(background)" as bg_ #Lavender {
card "Resumes a job\nsuspended in the background\nFree terminal" as c5
}
ctrlz -right-> fg_ : fg pour reprendre
ctrlz -right-> bg_ : bg pour continuer\nen arrière-plan
ctrlc -right-> c2 : Pas de retour\nprocessus tué
note bottom of ctrlz
Le processus est **figé**
mais **pas tué**
end note
note bottom of ampersand
Alternative : lancer
./gradlew build &
directement en arrière-plan
end note
@enduml
| Shortcut/Command | Signal | Effect | Go back? |
|---|---|---|---|
|
|
Suspend the process (frozen, in memory) |
Yes:`fg` ou |
|
|
Kill the process (terminated) |
No: process destroyed |
|
|
Kill the process + core dump |
No: process destroyed |
|
— |
Launch directly in the background |
`fg`to bring it to the foreground |
Ctrl+Z`isnon-destructive. The process is frozen as is, in memory. You can immediately resume with`fg. It’s a pause, not a stop.
|
The complete life cycle of a job
@startuml skinparam backgroundColor #FEFEFE skinparam defaultFontName sans-serif title Complete lifecycle of a shell job state "Non-existent" as none state "Foreground\n(occupies the terminal)" as foreground #LightCoral state "Suspended\n(Ctrl+Z / SIGTSTP)" as suspended #LightYellow state "Background\n(bg or &)" as background #LightGreen state "Done\n(exit code 0)" as done #LightGray [*] --> none none --> foreground : tape une commande foreground --> suspended : Ctrl+Z foreground --> done : fin normale suspended --> foreground : fg suspended --> background : bg suspended --> done : kill %n background --> foreground : fg background --> done : fin normale\n(en arrière-plan) background --> suspended : Ctrl+Z\n(si fg puis Ctrl+Z) done --> [*] note right of suspended L'état suspendu est le pivot de tout le système de jobs end note @enduml
Advanced tips
fg with the most recent job and the work-study
fg`without argument brings back the marked job+(the most recent). But you can also use%-`to target the previous job:
$ vim file1.txt
^Z
[1]+ Stoppé vim file1.txt
$ vim file2.txt
^Z
[2]+ Stoppé vim file2.txt
$ fg # ramène job [2] (le plus récent)
$ fg %- # ramène job [1] (le précédent)
to kill a job properly
$ jobs
[1]- Stoppé vim config.yml
[2]+ Stoppé ./gradlew test
$ kill %2 # envoie SIGTERM au job 2
$ kill -9 %1 # envoie SIGKILL au job 1 (force)
disown : detach from the shell
disown`remove a job from the shell’s jobs table. The process continues to run but can no longer be brought back with`fg:
$ ./gradlew build &
[1] 12345
$ disown %1
# Le processus continue mais n'est plus lié au shell
# Ça survives à la fermeture du terminal
Combine with nohup for long processes
In order for a process to survive terminal closure:
$ nohup ./gradlew build &
[1] 12345
$ disown %1
# Le build continue même si vous fermez le terminal
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontName sans-serif
rectangle "fg" as fg_ #SkyBlue {
card "Bring back the most recent job (+)" as f1
card "Target a specific\njob: fg %n" as f2
card "Alternate: fg %-" as f3
}
rectangle "kill %n" as kill_ #LightCoral {
card "SIGTERM (clean)" as k1
card "SIGKILL -9 (force)" as k2
}
rectangle "disown" as disown #Lavender {
card "Detach from shell\n(survives disconnection)" as d1
}
rectangle "nohup + &" as nohup_ #LightGreen {
card "Process immune\nto SIGHUP\n(survives terminal\nclosure)" as n1
}
fg_ -right-> kill_ : si faut\nterminer
kill_ -right-> disown_ : ou détacher
nohup_ -left-> fg_ : fg ramène en\navant-plan
note bottom of disown_
Après disown, fg ne peut
plus ramener le job
end note
@enduml
Common pitfalls and mistakes
Confuse Ctrl+Z and Ctrl+C
It’s the most common error.`Ctrl+C`kill the process.`Ctrl+Z`it suspends. If you press on`Ctrl+C`Reflexively, the entire state is lost — open files, LLM sessions, builds in progress.
@startuml
skinparam backgroundColor #FEFEFE
rectangle "Ctrl+Z (SIGTSTP)" as z #LightGreen {
card "Process frozen in memory" as z1
card "fg brings it back intact" as z2
card "No loss" as z3
}
rectangle "Ctrl+C (SIGINT)" as c #LightCoral {
card "Process completed" as c1
card "State permanently lost" as c2
card "No return possible" as c3
}
note bottom of z
Pause → Reprendre
end note
note bottom of c
Kill → Perdu
end note
@enduml
Jobs are local to the shell
jobs, fg et `bg`only work in the shell that launched the processes. If you open a new terminal, you will not see the other’s jobs.
# Terminal 1
$ vim file.txt
^Z
[1]+ Stoppé vim file.txt
# Terminal 2 (nouveau)
$ jobs
# (rien — les jobs sont locaux au shell)
To view system-wide processes, use`ps` ou htop :
ps aux | grep vim
Jobs do not survive shell closure
If you close the terminal, all suspended or background jobs are destroyed. For a process to survive, use`nohup`
+disown:
$ nohup ./gradlew build &
[1] 12345
$ disown %1
# Fermer le terminal → le build continue
Interactive processes and fg
Some interactive programs do not fare well after a`Ctrl+Z` + fg. It’s rare but it happens:
-
Some programs that use signals (vim, htop, less handle them well`Ctrl+Z`)
-
Long network programs (ssh, opencode) generally recover correctly
-
Programs that modify the terminal (ncurses) can sometimes leave the terminal in an unexpected state.
In case of corrupted terminal after a`fg`, type:
reset
Summary diagram
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontName sans-serif
skinparam ArrowColor #444444
title "fg — The tip that changes everything"
rectangle "**Problem**" as problem #LightCoral {
card "Terminal blocked\nby a process" as p1
card "Opening a new\nterminal = heavyweight solution" as p2
}
rectangle "**Solution**" as solution #LightGreen {
card "Ctrl+Z → suspend" as s1
card "fg → resumes" as s2
card "bg → in the background" as s3
card "jobs → see suspended" as s4
}
rectangle "**Advantages**" as advantages #SkyBlue {
card "No loss of context" as a1
card "No restart" as a2
card "A single terminal is enough" as a3
}
problem -right-> solution : "The native solution\nUnix for 40 years"
solution -right-> advantages : "Result"
note bottom of advantages
fg n'est pas un tips obscur
C'est un mécanisme fondamental
du shell Unix
end note
@enduml
Why fg is underestimated
Most modern developers discover`fg`late, maybe never. Some reasons:
-
IDEs hide the terminal: VS Code offers an integrated terminal, but multiple tabs make`fg`seemingly less necessary
-
Graphical tools replace CLI: file managers, system monitors, graphical Git clients
-
tmux and screen: these multiplexers solve the same problem differently, but`fg`is simpler and always available
However,`fg`is :
-
Universal: present in all POSIX shells (bash, zsh, fish…)
-
No dependence: no need to install tmux or screen
-
Snapshot: two strikes (
Ctrl+Z`then`fg) to suspend and resume -
Context Preserver: the process state is intact
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontName sans-serif
rectangle "Why is fg\nunderestimated?" as why #LightCoral {
card "IDE with multi-tab terminal" as w1
card "Graphical tools\nthat replace the CLI" as w2
card "tmux/screen\nsolve the problem\ndifferently" as w3
}
rectangle "Why is fg\nsuperior?" as superior #LightGreen {
card "Universal: bash,\nzsh, fish, sh..." as s1
card "No dependencies:\nno installation" as s2
card "Two keystrokes:\nCtrl+Z then fg" as s3
card "Preserved context:\nzero state loss" as s4
}
why -right-> superior : "Simplicity\nalways wins"
@enduml
Technical memorandum: the signals
For the curious, here are the signals involved:
| Signal | name | Sent by | Effect |
|---|---|---|---|
2 |
|
|
Interruption — terminates the process |
3 |
|
|
Exit with core dump |
18 |
|
|
Interactive stop — suspend the process |
19 |
|
|
Continue — resumes a suspended process |
15 |
|
|
Clean termination |
9 |
|
|
Forced termination (impossible to intercept) |
The full stream :
@startuml skinparam backgroundColor #FEFEFE skinparam defaultFontName sans-serif participant "User" as user participant "Shell" as shell participant "Process" as proc user -> shell : Ctrl+Z shell -> proc : SIGTSTP (signal 18) note right of proc : Processus figé\nen mémoire user -> shell : fg shell -> proc : SIGCONT (signal 19) note right of proc : Processus reprend\nen avant-plan user -> shell : bg shell -> proc : SIGCONT (signal 19) note right of proc : Processus reprend\nen arrière-plan user -> shell : Ctrl+C shell -> proc : SIGINT (signal 2) note right of proc : Processus terminé\nétat perdu @enduml
Conclusion
`fg`It’s not an obscure tip — it’s a fundamental Unix shell mechanism, available since the 1970s. The fact that most developers ignore it is a symptom of our era: IDEs and multiplexers have distanced us from the basics.
But when you are on an SSH server, in a minimal terminal, or simply when you’re using`opencode`and the LLM is hard at work —Ctrl+Z, fg, bg, `jobs`are your best friends.
_ Ctrl+Z suspend, fg resume. It’s the terminal’s pause‑play. _
References
-
man bash— section JOB CONTROL -
man signal— list of POSIX signals