Cross-Platform Guide, includes both Windows (PowerShell) and "Ubuntu" (ZSH) setup instructions.
| Concept | Git | jj |
|---|---|---|
| Working Copy | Staged changes before commit | Always a commit (@), saving a file immediately records it |
| History | Mutable (rebase rewrites) | Immutable by default, you create new revisions that replace old ones |
| Branches | git branch |
Bookmarks, jj bookmark |
| Repo Structure | .git/ folder |
Co-located: jj and git share the same .git folder |
| Conflicts | Must resolve before continuing | First-class objects, you can commit conflicts and resolve later |
| Undo | git reflog + manual recovery |
jj undo, universal Ctrl+Z for any operation |
winget install jj-vcs.jj# Using Homebrew
brew install jj
# Using Cargo (requires Rust >= 1.88)
cargo install --locked --bin jj jj-cli
# Arch Linux
pacman -S jujutsuSet your identity (required for commits):
jj config set --user user.name "Your Name"
jj config set --user user.email "your-email@example.com"- Windows:
%APPDATA%\jj\config.toml - Linux/macOS:
~/.config/jj/config.toml
View your config:
jj config list
jj config edit # Opens in your editorIf you don't have an SSH key yet, generate a secure Ed25519 key:
ssh-keygen -t ed25519 -C "your-email@example.com"This creates id_ed25519 (private) and id_ed25519.pub (public) in your ~/.ssh folder.
On windows, you can add the key to your SSH agent like this:
ssh-add PATH\\TO\\KEYCaution
The syntax of ~/ probably won't work on Windows. I recommend using the absolute path with \\
Add to your .gitconfig or repo's .git/config:
[user]
name = Your Name
email = your-email@example.com
signingkey = ~/.ssh/id_ed25519.pub
[commit]
gpgsign = true
[gpg]
format = sshNote
jj has its own signing config separate from Git. For jj to sign commits, add this to your jj config (jj config edit --user):
[user]
name = "Your Name"
email = "your-email@example.com"
[signing]
behavior = "own" # Sign only your own commits
backend = "ssh"
key = "~/.ssh/id_ed25519.pub"To verify signatures (including your own), you need an allowed_signers file. Without it, jj shows
File details:
- Location:
~/.ssh/allowed_signers(same folder as your keys)- Windows:
C:\Users\YourName\.ssh\allowed_signers - Linux/macOS:
~/.ssh/allowed_signers
- Windows:
- Extension: None (the file has no
.txtor other extension) - Format: One line per trusted key:
email key-type key-content
Create the file:
Windows (PowerShell)
# Replace YOUR_EMAIL with your jj user.email
"YOUR_EMAIL $(Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub)" | Out-File -FilePath "$env:USERPROFILE\.ssh\allowed_signers" -Encoding utf8Ubuntu/Linux (ZSH/Bash)
# Replace YOUR_EMAIL with your jj user.email
echo "YOUR_EMAIL $(cat ~/.ssh/id_ed25519.pub)" > ~/.ssh/allowed_signersThen add to your jj config:
[signing.backends.ssh]
# Windows (use absolute path with double backslashes):
allowed-signers = "C:\\Users\\YourName\\.ssh\\allowed_signers"
# Linux/macOS:
# allowed-signers = "~/.ssh/allowed_signers"π‘ Tip: If you use both jj and git commands, configure signing in both places.
The default jj log output is cluttered. This template organizes it into a clean 3-line format with emoji signature indicators.
Add to your jj config (jj config edit --user):
[template-aliases]
'format_sig(sig)' = 'if(sig, if(sig.status() == "good", "β
", "β οΈ"), "β")'
[templates]
log = '''
if(root,
format_root_commit(self),
label(
separate(" ",
if(current_working_copy, "working_copy"),
if(immutable, "immutable", "mutable"),
if(conflict, "conflicted"),
),
concat(
separate(" ",
format_short_change_id(change_id),
format_short_commit_id(commit_id),
format_sig(signature),
bookmarks,
tags,
working_copies,
) ++ "\n",
separate(" ",
if(empty, label("empty", "(empty)")),
if(conflict, label("conflict", "conflict")),
if(description, description.first_line(), description_placeholder),
) ++ "\n",
" " ++ format_timestamp(commit_timestamp(self))
++ " " ++ format_short_signature(author) ++ "\n",
),
)
)
'''Output:
@ abc123de 1a2b3c4d β
main feature-branch
Implement new feature
2026-01-15 14:00:00 your-email@example.com
β xyz789ab 5e6f7g8h β
Previous commit message
2026-01-14 10:30:00 other@example.com
| Line | Content |
|---|---|
| 1 | Change ID, Commit ID, Signature β
/ |
| 2 | Commit description |
| 3 | Timestamp + Author email |
Note
Custom templates may need updating when jj releases new versions.
Alternative: Built-in Minimal (no template override)
If you prefer to stay with the default single-line format but just want signature indicators, run:
jj config set --user ui.show-cryptographic-signatures trueThis shows [β]/[?]/[x] after each commit ID. Less organized but zero maintenance.
Add to your jj config (jj config edit --user):
[revset-aliases]
"wip()" = "description(exact:'') & mine()" # Empty description commits by you
"stale()" = "ancestors(@, 2) & empty()" # Find empty ancestors
[remotes.origin]
auto-track-bookmarks = "*" # Track all bookmarks from origin
# Or use a prefix pattern:
# auto-track-bookmarks = "yourprefix/*"Add to your PowerShell profile (notepad $PROFILE):
# ==============================================================================
# JJ VERSION CONTROL - POWERSHELL PROFILE
# ==============================================================================
# Dynamic Completions (recommended)
$env:COMPLETE = "powershell"
jj | Out-String | Invoke-Expression
Remove-Item Env:\COMPLETE
# Alternative: Standard completions
# jj util completion power-shell | Out-String | Invoke-Expression
# ==============================================================================
# CORE FUNCTIONS
# ==============================================================================
# Initialize jj in an existing Git directory
function ji {
param($Branch = "main")
jj git init --colocate
jj bookmark track $Branch@origin
}
# Pass-through: Run any jj command
function j { jj @args }
# Log commands (signature status shown via custom template: β
/β οΈ/β)
function jl { jj log @args }
function jla { jj log -r "all()" }
function js { jj status @args }
function jsh { jj show @args } # Show specific revision
# ==============================================================================
# NAVIGATION
# ==============================================================================
function je { jj edit @args } # Edit: switch to commit
function jep { jj edit "@-" } # Edit Parent
function jen { jj edit @args; jj new } # Edit + New child
function jnext { jj next @args } # Move to child revision
function jprev { jj prev @args } # Move to parent revision
# ==============================================================================
# INSPECTION
# ==============================================================================
function jd { jj diff @args }
function jds { jj diff --stat @args }
function jevo { jj evolog @args } # Evolution log
function jidiff { jj interdiff @args } # Compare diffs of two revisions
# ==============================================================================
# ACTIONS
# ==============================================================================
function jn { jj new @args } # New commit
function jdesc { jj describe @args } # Set commit message
function jme { jj metaedit @args } # Edit metadata only (no content change)
function ja { jj abandon @args } # Abandon commits
function jundo { jj undo @args } # Universal undo
function jrestore { param($From) jj restore --from $From }
function jdup { jj duplicate @args } # Duplicate commit
function jdupe {
# Convert args to string for the success message (defaults to '@' if empty)
$target = if ($args) { "$args" } else { "@" }
# 1. Run duplicate silently by capturing all output
$output = jj duplicate @args 2>&1
# 2. Find the new ID (captures text after "as")
if ("$output" -match 'as\s+([a-z0-9]+)') {
$newId = $Matches[1]
# 3. Switch to the new revision (silencing the edit confirmation too)
jj edit $newId 2>&1 | Out-Null
# 4. Print your custom message
Write-Host "duplicated `"$target`" is now @" -ForegroundColor Green
}
else {
# If the regex fails, it usually means jj returned an error
# So we print the captured output to show you what happened
Write-Error "$output"
}
}
function jabs { jj absorb @args } # Absorb changes into mutable stack
function jfix { jj fix @args } # Apply formatting fixes
function jpar { jj parallelize @args } # Make revisions siblings
# ==============================================================================
# SPLITTING & SQUASHING
# ==============================================================================
function jsq { jj squash @args } # Squash into parent
function jsqi { param($Target) jj squash --into $Target }
function jsqp { jj squash -i @args } # Interactive squash
function jsplit { jj split @args } # Split commit interactively
# ==============================================================================
# REBASING
# ==============================================================================
function jrb { jj rebase @args } # Rebase
function jrbd { param($Dest) jj rebase -d $Dest } # Rebase to destination
function jrbs { param($Source, $Dest) jj rebase -s $Source -d $Dest }
# ==============================================================================
# BISECT (find bad revisions)
# ==============================================================================
function jbis { jj bisect @args } # Bisect to find bad revision
# ==============================================================================
# IGNORE IMMUTABLE (force modify immutable commits)
# ==============================================================================
function jjii { jj --ignore-immutable @args } # Run jj ignoring immutability
# ==============================================================================
# CONFLICT RESOLUTION
# ==============================================================================
function jres { jj resolve @args } # Launch merge tool
function jresl { jj resolve --list } # List conflicted files
# ==============================================================================
# BOOKMARK MANAGEMENT
# ==============================================================================
function jb { jj bookmark @args }
function jbc { jj bookmark create @args }
function jbd { jj bookmark delete @args }
function jbr { jj bookmark rename @args }
function jbt { jj bookmark track @args }
function jbu { jj bookmark untrack @args }
function jbm {
param($Name, $Target="@")
jj bookmark move $Name --to $Target --allow-backwards
}
function jbdr {
param($Name, $Remote = "origin")
Write-Host "Deleting bookmark '$Name' from remote '$Remote'..." -ForegroundColor Yellow
git push $Remote --delete $Name @args
}
# ==============================================================================
# REMOTE MANAGEMENT
# ==============================================================================
function jr { jj git remote add @args }
function jrr { jj git remote remove @args }
function jrl { jj git remote list }
# ==============================================================================
# GIT SYNC
# ==============================================================================
function jgf { jj git fetch @args }
function jgp {
param($Bookmark)
if ($Bookmark) {
jj git push --bookmark $Bookmark @args
} else {
jj git push @args
}
}
# ==============================================================================
# OPERATION LOG
# ==============================================================================
function jop { jj op log @args } # View operation history
function jopu { jj op undo @args } # Undo specific operation
function jopr { jj op restore @args } # Restore to operation state
# ==============================================================================
# MAINTENANCE
# ==============================================================================
function jgc { jj util gc @args }
function jclean {
$revs = 'all() ~ ::(bookmarks() | @) ~ root()'
Write-Host "π Scanning for abandoned commits..." -ForegroundColor Cyan
$count = (jj log -r $revs --no-graph | Measure-Object).Count
if ($count -eq 0) {
Write-Host "β¨ Repository is already clean." -ForegroundColor Green
return
}
jj log -r $revs --no-graph
Write-Host "`nβ οΈ Found $count disconnected commits." -ForegroundColor Yellow
$confirm = Read-Host "Delete them? (y/n)"
if ($confirm -eq 'y') {
jj abandon -r $revs
Write-Host "β
Cleaned up." -ForegroundColor Green
} else {
Write-Host "β Cancelled." -ForegroundColor Red
}
}
# ==============================================================================
# FILE OPERATIONS
# ==============================================================================
function jfshow { jj file show @args } # Show file from revision
function jflist { jj file list @args } # List files in revisionAdd to your ~/.zshrc:
Warning
Since I'm using Powershell, I have not tested if all of them work
# ==============================================================================
# JJ VERSION CONTROL - ZSH CONFIGURATION
# ==============================================================================
# Dynamic Completions (recommended)
source <(COMPLETE=zsh jj)
# Alternative: Standard completions
# autoload -U compinit
# compinit
# source <(jj util completion zsh)
# ==============================================================================
# CORE FUNCTIONS
# ==============================================================================
# Initialize jj in an existing Git directory
ji() {
local branch="${1:-main}"
jj git init --colocate
jj bookmark track "${branch}@origin"
}
# Pass-through: Run any jj command
j() { jj "$@" }
# Log commands (signature status shown via custom template: β
/β οΈ/β)
jl() { jj log "$@" }
jla() { jj log -r "all()" }
js() { jj status "$@" }
jsh() { jj show "$@" }
# ==============================================================================
# NAVIGATION
# ==============================================================================
je() { jj edit "$@" }
jep() { jj edit "@-" }
jen() { jj edit "$@" && jj new }
jnext() { jj next "$@" }
jprev() { jj prev "$@" }
# ==============================================================================
# INSPECTION
# ==============================================================================
jd() { jj diff "$@" }
jds() { jj diff --stat "$@" }
jevo() { jj evolog "$@" }
jidiff() { jj interdiff "$@" }
# ==============================================================================
# ACTIONS
# ==============================================================================
jn() { jj new "$@" }
jdesc() { jj describe "$@" }
jme() { jj metaedit "$@" }
ja() { jj abandon "$@" }
jundo() { jj undo "$@" }
jrestore(){ jj restore --from "$1" }
jdup() { jj duplicate "$@" }
jdupe() {
# 1. Define target for the text message (defaults to "@" if args are empty)
local target="${*:-@}"
# 2. Run duplicate silently, merging stderr into stdout
# --color=never ensures regex matches clean text
local output
output=$(jj duplicate --color=never "$@" 2>&1)
# 3. Regex match looking for "as <id>"
if [[ "$output" =~ "as ([a-z0-9]+)" ]]; then
# Zsh stores capture groups in the $match array
local new_id=$match[1]
# 4. Switch to new ID silently
jj edit "$new_id" > /dev/null 2>&1
# 5. Print success message (using print -P for colors)
print -P "%F{green}duplicated \"$target\" is now @%f"
else
# If match failed, print the error output from jj
print -u2 "$output"
fi
}
jabs() { jj absorb "$@" }
jfix() { jj fix "$@" }
jpar() { jj parallelize "$@" }
# ==============================================================================
# SPLITTING & SQUASHING
# ==============================================================================
jsq() { jj squash "$@" }
jsqi() { jj squash --into "$1" }
jsqp() { jj squash -i "$@" }
jsplit() { jj split "$@" }
# ==============================================================================
# REBASING
# ==============================================================================
jrb() { jj rebase "$@" }
jrbd() { jj rebase -d "$1" }
jrbs() { jj rebase -s "$1" -d "$2" }
# ==============================================================================
# BISECT (find bad revisions)
# ==============================================================================
jbis() { jj bisect "$@" }
# ==============================================================================
# IGNORE IMMUTABLE (force modify immutable commits)
# ==============================================================================
jjii() { jj --ignore-immutable "$@" }
# ==============================================================================
# CONFLICT RESOLUTION
# ==============================================================================
jres() { jj resolve "$@" }
jresl() { jj resolve --list }
# ==============================================================================
# BOOKMARK MANAGEMENT
# ==============================================================================
jb() { jj bookmark "$@" }
jbc() { jj bookmark create "$@" }
jbd() { jj bookmark delete "$@" }
jbr() { jj bookmark rename "$@" }
jbt() { jj bookmark track "$@" }
jbu() { jj bookmark untrack "$@" }
jbm() {
local name="$1"
local target="${2:-@}"
jj bookmark move "$name" --to "$target" --allow-backwards
}
jbdr() {
local name="$1"
local remote="${2:-origin}"
echo -e "\e[33mDeleting bookmark '$name' from remote '$remote'...\e[0m"
git push "$remote" --delete "$name"
}
# ==============================================================================
# REMOTE MANAGEMENT
# ==============================================================================
jr() { jj git remote add "$@" }
jrr() { jj git remote remove "$@" }
jrl() { jj git remote list }
# ==============================================================================
# GIT SYNC
# ==============================================================================
jgf() { jj git fetch "$@" }
jgp() {
if [[ -n "$1" ]]; then
jj git push --bookmark "$1" "${@:2}"
else
jj git push "$@"
fi
}
# ==============================================================================
# OPERATION LOG
# ==============================================================================
jop() { jj op log "$@" }
jopu() { jj op undo "$@" }
jopr() { jj op restore "$@" }
# ==============================================================================
# MAINTENANCE
# ==============================================================================
jgc() { jj util gc "$@" }
jclean() {
local revs='all() ~ ::(bookmarks() | @) ~ root()'
echo -e "\e[36mπ Scanning for abandoned commits...\e[0m"
local count=$(jj log -r "$revs" --no-graph | wc -l)
if [[ $count -eq 0 ]]; then
echo -e "\e[32m⨠Repository is already clean.\e[0m"
return
fi
jj log -r "$revs" --no-graph
echo -e "\n\e[33mβ οΈ Found $count disconnected commits.\e[0m"
read -q "confirm?Delete them? (y/n) "
echo
if [[ $confirm == "y" ]]; then
jj abandon -r "$revs"
echo -e "\e[32mβ
Cleaned up.\e[0m"
else
echo -e "\e[31mβ Cancelled.\e[0m"
fi
}
# ==============================================================================
# FILE OPERATIONS
# ==============================================================================
jfshow() { jj file show "$@" }
jflist() { jj file list "$@" }| Goal | Alias | Command |
|---|---|---|
| Initialize Repo | ji [branch] |
jj git init --colocate + track branch |
| Pass-through | j <args> |
Run any raw jj command |
| Goal | Alias | Command |
|---|---|---|
| Switch to commit | je <id> |
Makes <id> the new @ |
| Go back one step | jep |
Moves @ to parent |
| Next child | jnext |
Move working copy to child revision |
| Previous parent | jprev |
Move working copy to parent revision |
| View Log | jl |
Shows commit graph (with β
/ |
| View All | jla |
Shows ALL commits (with signing status) |
| Show Revision | jsh <id> |
Shows specific revision details |
| Diff vs Parent | jd |
Shows working copy changes |
| Diff Summary | jds |
File statistics |
| Evolution Log | jevo |
History of a revision's changes |
| Interdiff | jidiff |
Compare diffs of two revisions |
| Goal | Alias | Command |
|---|---|---|
| New Commit | jn |
Creates child. jn - creates sibling |
| Describe | jdesc -m "msg" |
Set commit message |
| Metaedit | jme |
Edit metadata only (no content) |
| Squash | jsq |
Merge @ into parent |
| Squash Into | jsqi <id> |
Merge @ into specific commit |
| Interactive Squash | jsqp |
Pick specific hunks |
| Split Commit | jsplit |
Split into multiple commits (TUI) |
| Duplicate | jdup <id> |
Clone commit with same parent |
| Duplicate + Edit | jdupe <id> |
Clone commit and edit the new copy |
| Absorb | jabs |
Absorb changes into mutable stack |
| Fix | jfix |
Apply formatting fixes |
| Parallelize | jpar |
Make revisions siblings |
| Undo | jundo |
Reverts last operation |
| Goal | Alias | Command |
|---|---|---|
| Rebase | jrb |
General rebase passthrough |
| Rebase to Dest | jrbd <dest> |
Rebase @ onto destination |
| Rebase Source | jrbs <src> <dest> |
Rebase source tree to destination |
| Goal | Alias | Command |
|---|---|---|
| Resolve | jres |
Launch merge tool |
| List Conflicts | jresl |
Show files with conflicts |
| Goal | Alias | Command |
|---|---|---|
| Find Bad Commit | jbis |
Binary search for bad rev |
| Goal | Alias | Command |
|---|---|---|
| Create | jbc <name> |
Create on current commit |
| Delete | jbd <name> |
Delete locally |
| Rename | jbr <old> <new> |
Rename bookmark |
| Track | jbt <name>@origin |
Track remote bookmark |
| Untrack | jbu <name>@origin |
Stop tracking |
| Move | jbm <name> [id] |
Move to @ or specific ID |
| Remote Delete | jbdr <name> |
Delete from remote |
| Goal | Alias | Command |
|---|---|---|
| Add | jr <name> <url> |
Add remote |
| Remove | jrr <name> |
Remove remote |
| List | jrl |
List all remotes |
| Goal | Alias | Command |
|---|---|---|
| Fetch | jgf |
Fetch all. jgf origin for specific |
| Push | jgp [bookmark] |
Push all or specific bookmark |
| Goal | Alias | Command |
|---|---|---|
| View Ops | jop |
Show operation history |
| Undo Op | jopu |
Undo specific operation |
| Restore Op | jopr <id> |
Restore to operation state |
| Goal | Alias | Command |
|---|---|---|
| Garbage Collect | jgc |
Clean unreferenced files |
| Deep Clean | jclean |
Interactive abandon disconnected commits |
| Goal | Alias | Command |
|---|---|---|
| Bypass Immutability | jjii <args> |
Run any jj command ignoring immutable |
| Goal | Alias | Command |
|---|---|---|
| Show File | jfshow <path> -r <rev> |
Show file from revision |
| List Files | jflist -r <rev> |
List files in revision |
When using jsplit (or jj split -i), these keyboard shortcuts apply:
| Key | Action |
|---|---|
? |
Show help |
q |
Quit/cancel |
c |
Confirm and proceed |
β/k |
Previous item |
β/j |
Next item |
β/h |
Outer item (fold) |
β/l |
Inner item (unfold) |
Space |
Toggle selection |
Enter |
Toggle + advance |
a |
Invert all selections |
f |
Expand/collapse section |
F |
Expand/collapse all |
e |
Edit commit message |
Problem: main is immutable (β) because it was pushed. You need to modify it.
Solution: Create a mutable replacement, bring it up to date, then move the bookmark.
jdup mainOption A: Squash specific commit
jsqi xyzabcOption B: Squash a range
jj squash -r "main@origin..@" --into @jd --from main@origin --to @jbm mainjgp mainWhat are immutable commits?
By default, jj prevents rewriting commits that are ancestors of:
trunk()(usuallymain@origin)tags()untracked_remote_bookmarks()
These show as β (diamond) in jj log instead of β (circle). The root commit is always immutable.
When to use --ignore-immutable:
Use this flag when you genuinely need to modify a commit that jj considers immutable, for example, fixing a typo in a commit message on main before others have pulled it.
Warning
This allows rewriting any commit and all its descendants without warning. Use wisely, remember jj undo exists.
Example: Fix a typo in an immutable commit's message
# This would normally fail because main is immutable:
jj describe main -m "Fixed commit message"
# Error: Commit abc123 is immutable
# With the flag (or alias), it works:
jjii describe main -m "Fixed commit message"Example: Squash a fix into an already-pushed commit
# You found a bug in an immutable commit and want to amend it:
jjii squash --into mainOr use the flag directly:
jj --ignore-immutable describe main -m "Fixed message"The jjii alias is simply jj --ignore-immutable, so you can use any jj command after it.
jj allows you to commit conflicts and resolve them later, no "merge in progress" blocking state.
-
See what's conflicted:
jresl # Lists conflicted files -
Launch merge tool:
jres # Opens configured merge tool # Or for specific file: jres path/to/file.txt
-
Manual resolution (if preferred):
- Edit the file to remove conflict markers (
<<<<<<<,=======,>>>>>>>) - jj automatically detects the resolution on next operation
- Edit the file to remove conflict markers (
-
Continue working:
jdesc -m "Resolved conflicts"
Add to jj config:
[ui]
merge-editor = "code -w" # VS Code
# merge-editor = "nvim" # Neovim
# merge-editor = ":builtin" # Built-in TUIjn -m "Feature: Login"
jbc feature-login# Do work...
jdesc -m "Implement login form"
jgp feature-login# After PR merged...
jbrr feature-login # Remove from remote
jbd feature-login # Remove local bookmark
jclean # Clean up orphaned commits# Show all your work-in-progress
jl -r "mine() & mutable()"
# Find empty commits
jl -r "empty()"
# Commits between main and current
jl -r "main..@"
# All descendants of main
jl -r "main::"
# Commits with a specific description
jl -r 'description("fix")'
# All commits by author
jl -r 'author("name")'
# Commits touching a file
jl -r 'file("src/main.rs")'-
Always start fresh after switching context:
jn # Creates a clean child commit -
Use descriptive bookmarks:
jbc yourname/feature-description
-
Regularly clean up:
jclean # Remove disconnected commits jgc # Garbage collect
-
Signature status is shown inline:
jl # Look for β (good), β οΈ (warning), β (unsigned) -
Use the operation log to recover:
jop # Find the operation ID jopr <id> # Restore to that state
-
Preview before destructive operations:
jd --from <old> --to <new>
-
Beware of creating siblings at root:
If you run
jn -(new sibling) when you are at a child of root, you create a new root commit. If there is no.gitignorein this new empty commit, everything (includingnode_modules) becomes tracked.Fix: Delete the massive tracked folder to untrack it, or just
jundo. -
Restoring ignored files (.env):
If your
.gitignoreis only local, and you restore a version where it's missing (or switch to a root sibling),jjmight delete previously ignored files like.envbecause they are technically "untracked and not ignored" in the new state, or simply due to working copy updates.Fix:
jundorestores them. -
Use a Global Gitignore:
Link a global ignore file in your gitconfig to protect files like
**/.enveven without a local.gitignore. This prevents them from being accidentally deleted or tracked.In
.gitconfig:[core] excludesfile = ~/.gitignore_global
(Windows path example:
C:/Users/YourName/.gitignore_global)In
~/.gitignore_global:**/.env .DS_Store node_modules/