reading time : 10 minutes

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:

  1. No loss of context: the LLM session, conversation history, open files — everything is preserved

  2. 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

Ctrl+Z

Suspend the process in the foreground

Sending of`SIGTSTP`

fg

Resume the default job in the foreground

fg ou fg %1

fg %n

Resume job n in the foreground

fg %2

bg

Resume the default job in the background

bg ou bg %1

bg %n

Resume job n in the background

bg %2

jobs

List the jobs of the current shell

jobs -l(with PID)

kill %n

Finish the job n

kill %2

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?

Ctrl+Z

SIGTSTP

Suspend the process (frozen, in memory)

Yes:`fg` ou bg

Ctrl+C

SIGINT

Kill the process (terminated)

No: process destroyed

Ctrl+\

SIGQUIT

Kill the process + core dump

No: process destroyed

commande &

—

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:

  1. IDEs hide the terminal: VS Code offers an integrated terminal, but multiple tabs make`fg`seemingly less necessary

  2. Graphical tools replace CLI: file managers, system monitors, graphical Git clients

  3. tmux and screen: these multiplexers solve the same problem differently, but`fg`is simpler and always available

However,`fg`is :

  1. Universal: present in all POSIX shells (bash, zsh, fish…​)

  2. No dependence: no need to install tmux or screen

  3. Snapshot: two strikes (Ctrl+Z`then`fg) to suspend and resume

  4. 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

SIGINT

Ctrl+C

Interruption — terminates the process

3

SIGQUIT

Ctrl+\

Exit with core dump

18

SIGTSTP

Ctrl+Z

Interactive stop — suspend the process

19

SIGCONT

fg/bg

Continue — resumes a suspended process

15

SIGTERM

kill %n

Clean termination

9

SIGKILL

kill -9 %n

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

Related articles