A cross-platform PowerShell profile with a custom or Oh My Posh prompt, a once-a-day system banner, Gemini-powered helpers, pip safety wrappers, math functions and small everyday utilities. The code has Windows, Linux and macOS code paths, but it has only been runtime-verified on Linux (see Platform status).
- ๐จ Customizable Prompt: two-line custom prompt with 12 color schemes, a lightweight Git status, or an optional Oh My Posh engine with a bundled Gruvbox theme
- ๐ฅ๏ธ System Banner: neofetch-style summary, shown at most once per calendar day, or on demand with
sysinfo - ๐ค AI Integration: Google Gemini chat with terminal-optimized responses
- ๐ฌ Smart Commit Messages: Gemini-suggested commit messages based on staged changes
- ๐งฎ Mathematical Functions: thin wrappers around .NET
[Math] - ๐ Pip Wrappers: warning before
pip installoutside a virtual environment - ๐ Copilot wrappers (legacy):
ghcs/ghcetarget the archivedgh-copilotextension (see Optional Dependencies) - ๐ Local API Key Storage: DPAPI on Windows, Keychain or OpenSSL on macOS, OpenSSL on Linux
- ๐ File Operations: directory trees and recursive file dumps with simplified
.gitignorehandling - โ๏ธ JSON Configuration: persistent settings in
powershell.config.json - โก On-demand loading: rarely used parts are loaded on first use (see Profile Layout)
- PowerShell 7+. Developed and run on 7.6;
ghcs/ghceuse thecleanblock, which needs 7.3+. Windows PowerShell 5.1 has not been tried. - Git (prompt status, shortcuts, installation)
- Everything else is optional; see Optional Dependencies
The profile is modular. Microsoft.PowerShell_profile.ps1 is only a loader that dot-sources the files in Profile/, so copying the loader alone does not work (it warns Profile part not found for each part and then stops with a "Cannot index into a null array" error). Install the loader, the whole Profile/ folder and, if you want the Oh My Posh theme, Themes/.
-
Find your real profile directory. Do not assume
~/Documents/PowerShell: on Windows the Documents folder can be redirected (OneDrive, a network share), and on Linux/macOS it normally is~/.config/powershell.$PROFILE # full path of the CurrentUserCurrentHost profile Split-Path -Parent $PROFILE # the directory to install into
-
Back up what is already there and install (run in
pwsh). The backup copies only the files this profile would replace; nothing is deleted, and files that already exist are overwritten by the new copies after the backup is taken.$dir = Split-Path -Parent $PROFILE New-Item -ItemType Directory -Path $dir -Force | Out-Null # Back up whatever would be replaced $backup = Join-Path $dir ('profile-backup-' + (Get-Date -Format 'yyyyMMdd-HHmmss')) New-Item -ItemType Directory -Path $backup -Force | Out-Null foreach ($name in 'Microsoft.PowerShell_profile.ps1', 'powershell.config.json', 'Profile', 'Themes') { $item = Join-Path $dir $name if (Test-Path -LiteralPath $item) { Copy-Item -LiteralPath $item -Destination $backup -Recurse } } # Install the loader and the complete modular tree $src = Join-Path ([IO.Path]::GetTempPath()) ('PowerShell-Config-' + [guid]::NewGuid()) git clone https://github.com/adrian-cancio/PowerShell-Config.git $src Copy-Item -LiteralPath (Join-Path $src 'Microsoft.PowerShell_profile.ps1'), (Join-Path $src 'Profile'), (Join-Path $src 'Themes') -Destination $dir -Recurse -Force
Files that no longer exist upstream are not removed from an older
Profile/folder; the loader only reads the files it lists, so leftovers are harmless. -
Start a new
pwshsession (preferred over. $PROFILE, which re-runs the profile inside the current session). -
Configure your settings (optional). On first start the profile writes
powershell.config.jsonwith the defaults next to your profile. See Configuration Options and the note above it about that file being shared with PowerShell itself.
Only copy
powershell.config.jsonover an existing one deliberately. It may already hold your own PowerShell settings (for exampleMicrosoft.PowerShell:ExecutionPolicy).
# Weather (network: wttr.in). Without a place it first asks ipinfo.io for your city.
Get-Weather "Madrid"
# Show directory tree
tree
# Copy current path to clipboard
cpwd
# Public IP (network request to https://api.ipify.org; see the privacy note under "System banner")
Get-PublicIP
# Show disk space
Get-DiskSpace
# Smart pip installations (with global warnings)
pip install package-name # Warns if no venv/conda environment is active
pip install package-name --global # Skips the warningGet-Weather parameters are -Place, -Language (defaults to your culture's two-letter code, or en) and -MaxAttempts (1-20, default 3, only used for the automatic location lookup).
These commands send data to external services and need credentials or tools you provide yourself. Nothing is called until you run them.
# Start a Gemini chat (alias: gemini-chat); the first argument is -InitialPrompt
gemini-chat "how to find large files in PowerShell"
Invoke-GeminiChat -Model "gemini-flash-latest" -ResetApiKey
# Suggest a commit message from the staged changes (alias: sgcm)
sgcm
Invoke-SuggestCommitMessage -CommitCount 50 -Model "gemini-flash-latest"
Invoke-SuggestCommitMessage -Force # Commit without asking for confirmation
Invoke-SuggestCommitMessage -AdditionalInstructions "Use conventional commits format"
# Legacy GitHub Copilot wrappers (see Optional Dependencies before relying on them)
ghcs "compress a folder"
ghce "Get-ChildItem -Recurse | Where-Object {$_.Length -gt 100MB}"The default model is the gemini-flash-latest alias. Invoke-SuggestCommitMessage sends the staged diff, the branch name and up to -CommitCount (default 100) recent commit subjects to the Gemini API, so do not use it on repositories whose contents you cannot share with Google.
# Calculate trigonometric functions
Get-Sin 1.5708 # ฯ/2
Get-Cos 0 # 1
Get-Sqrt 16 # 4
Get-Pow 2 3 # 8
# Constants are available straight from .NET
[Math]::PI
[Math]::EThe math functions live in Profile/Math.psm1 (20 exported functions, listed in the Function Reference) and are imported at startup unless "Microsoft.PowerShell.Profile:EnableMathModule": false. Several names (Get-Max, Get-Min, Get-Round, ...) are generic; they are thin wrappers over [Math]: Get-Max and Get-Min compare exactly two values (Get-Max 1 2), and Get-Round takes an optional -Digits.
The profile includes intelligent wrappers for pip and pip3 commands that warn you when installing packages globally outside of a virtual environment:
# Installing globally will show a warning
pip install requests
# Output:
# โ ๏ธ WARNING: Installing packages globally (not in virtual environment).
# Recommended: python -m venv venv then activate it.
# Or use: pip install requests --global
#
# Press Enter to continue or Ctrl+C to cancel.
# Skip warning with --global flag
pip install requests --global
# In a virtual environment, works normally (no warning)
python -m venv myenv
.\myenv\Scripts\Activate.ps1 # Windows
./myenv/bin/Activate.ps1 # Linux/macOS
pip install requests # No warning shownBehavior:
- Warns only when the first argument is
installand no environment is detected. - Detects environments through
$env:VIRTUAL_ENV(venv/virtualenv) and$env:CONDA_DEFAULT_ENV(Conda). --globalis removed before calling pip, but only on the path where the warning would have been shown. Inside an active environment, do not pass it (pip would receive the unknown flag).- The pause (
Press Enter to continue) only happens in an interactive session; with redirected input the warning is printed and the install continues. - All other pip commands are passed through unchanged.
pipandpip3resolve their own executables independently (the first application found onPATH, cached for the session).
Two engines are available, selected with Microsoft.PowerShell.Profile:PromptEngine:
| Value | Behavior |
|---|---|
Custom (default) |
The built-in two-line prompt described under Prompt Customization. |
OhMyPosh |
Uses an oh-my-posh executable that is already installed and on PATH, with the theme set in Microsoft.PowerShell.Profile:OhMyPoshTheme (default Themes/gruvbox.omp.json). Nothing is downloaded. |
Any other PromptEngine value behaves like Custom (the comparison with OhMyPosh is case-insensitive). To switch, edit powershell.config.json and open a new terminal:
{
"Microsoft.PowerShell.Profile:PromptEngine": "OhMyPosh",
"Microsoft.PowerShell.Profile:OhMyPoshTheme": "Themes/gruvbox.omp.json"
}Theme path. A relative path is resolved against the profile folder (the folder that holds the loader), so the bundled theme needs the Themes/ directory installed next to Microsoft.PowerShell_profile.ps1. Absolute local paths also work; ~ is not expanded. The theme file must be valid JSON, otherwise the profile falls back (see below).
Precedence and fallback. DefaultPrompt: true wins over both engines. If oh-my-posh is not found, the theme is missing or is not valid JSON, oh-my-posh init fails or returns nothing, or no prompt gets installed, the profile prints a warning and keeps the Custom prompt. To roll back, set PromptEngine back to Custom and open a new terminal rather than reloading inside a session where Oh My Posh is already initialized.
What the bundled Gruvbox theme shows. Two lines. The first has the OS icon, optional context tags, user@host | path, an optional Python environment, the failed-command marker (โ plus the exit code) and the execution time when it is at least 2 seconds (hard-coded in the theme, independent of PromptMinDurationSeconds). The second has the Git segment (branch or commit, ahead/behind, staged/working changes, โ when clean) and the |> marker, red after a failed command. It does not display the PowerShell version or the profile load time; use Show-ProfileInfo for load timings.
Context tags appear only when detected:
| Tag | Shown when |
|---|---|
[SSH] |
Oh My Posh reports an SSH session |
[WSL] |
Oh My Posh reports WSL, or WSL_DISTRO_NAME is set |
[CONTAINER] |
the CONTAINER or DISTROBOX_ENTERED environment variable is set (variable names are case-sensitive on Linux/macOS, so a lowercase container variable does not match) |
[ADMIN] |
the process runs as root/administrator |
[py:name] |
a Python virtual or Conda environment is active (an installed Python or a Python project alone does not trigger it) |
PromptColorScheme, GitPrompt, GitPromptTimeoutMs, PromptMinDurationSeconds and EnableRandomTitle only affect the Custom engine; to change the Oh My Posh look, edit the JSON theme. The theme uses icon glyphs for the OS and the branch, so a Nerd Font is probably needed to render them.
Tests. Run from the repository root:
pwsh -NoProfile -NonInteractive -File Tests/Prompt.Tests.ps1
pwsh -NoProfile -NonInteractive -File Tests/Prompt.Context.Tests.ps1 -ScratchRoot <scratch-directory>
pwsh -NoProfile -NonInteractive -File Tests/Profile.Lazy.Tests.ps1 -ScratchRoot <scratch-directory>Prompt.Tests.ps1parses everyProfile/*.ps1and exercises engine selection and the fallbacks, including a real activation, so it needsoh-my-poshinstalled.Prompt.Context.Tests.ps1renders the theme with the realoh-my-poshand a throwaway Pythonvenv(no pip). SSH, WSL, container and Conda inputs are simulated through environment variables in child processes;[ADMIN]is compared against the real identity and elevation is never simulated.Profile.Lazy.Tests.ps1starts childpwshprocesses withHOME,XDG_*,LOCALAPPDATAandAPPDATApointed at a scratch directory, and checks startup, lazy loading, completion and the system banner. The public IP request is simulated, so no network is needed. The scratch directories are not deleted afterwards.
When Microsoft.PowerShell.Profile:ShowSystemInfo is true (the default) the profile prints a neofetch-style SYSTEM banner at most once per calendar day. You can always print it on demand:
sysinfo # alias of Show-SystemInfo; ignores the once-a-day marker
sysinfo -PublicIP # also look up the public IP address (network request)Fields. Rows that cannot be determined on the current platform are omitted. Rendered is the time taken to build the banner on your machine.
| Row | Source | Platforms |
|---|---|---|
User, OS (with architecture), Local IP, Rendered |
.NET, plus ip -brief addr on Linux |
all |
Up |
/proc/uptime, Win32 CIM, sysctl kern.boottime |
Linux, Windows, macOS |
Mem |
/proc/meminfo, Win32 CIM, sysctl hw.memsize (macOS shows only the total) |
all (n/a if unavailable) |
Load |
/proc/loadavg, sysctl vm.loadavg |
Linux, macOS |
Kernel, Pkgs (rpm -qa, else dpkg-query), Term, CPU, Disk (df -h /), Bat (/sys/class/power_supply/BAT1) |
local files and commands | Linux only |
WM |
hard-coded to the text Niri |
Linux only (see Known limitations) |
Public IP |
https://ifconfig.me/ip, then https://api.ipify.org |
only with -PublicIP or ShowPublicIP |
The banner does not show the shell name or version. On Linux it runs external programs (rpm/dpkg-query, df, ip) and on macOS sysctl; counting packages in particular can take a noticeable fraction of a second on some systems, so no fixed render time is promised. Read the Rendered row on your machine.
Once-per-day policy (as implemented). On startup the profile looks for a marker file named sysinfo-<yyyyMMdd> (local date) in $HOME/.local/share/powershell-profile/, on every platform including Windows. If it is missing, the banner is printed and the empty marker is created; if it exists, nothing is printed. What follows directly from the code:
- The first session of the day that loads this profile uses up the banner, whatever terminal, host or action started it. The code does not limit the banner to a particular key binding or terminal shortcut, nor to SSH sessions, nor to any other trigger.
- There is no interactive-session guard. A non-interactive
pwsh -Command ...orpwsh -File ...that loads the profile also prints the banner (to the host, so it can end up in captured output) and consumes the day's marker. Use-NoProfilefor scripts, or setShowSystemInfotofalse. - The marker directory is separate from the profile data directory (see Other files and locations), and old markers are never cleaned up (one empty file per day the banner was shown). Delete today's file to see the banner again on the next start.
Privacy and network. With -PublicIP, or when Microsoft.PowerShell.Profile:ShowPublicIP is true, the banner sends an HTTPS request to ifconfig.me, and to api.ipify.org if that fails, with a 3-second timeout each; if both fail it shows n/a. Those services see your public address and ordinary request metadata. With ShowPublicIP enabled, this request happens during the startup that prints the banner. ShowPublicIP is false by default. Get-PublicIP uses https://api.ipify.org with no explicit timeout. Separately, Local IP is the first IPv4 address of an active, non-loopback interface (Linux: the first UP interface reported by ip -brief addr); on a host with a directly assigned public address (some servers, VPS or cloud instances) that address is public. Treat banner output as sensitive when sharing screenshots or logs.
This section describes the Custom engine. The prompt is two lines. An OS icon (โ Windows, ฮป Linux, โ macOS) comes first, then user@hostname and the current path (the home directory is shortened to ~). The second line holds the Git status and the prompt marker:
โ ||user@host|-|~\Code\project|| โ1 3.2s
|main โ2โ1 +1 ~3 ?2 $1|>
โNappears when the last command failed.Nis the exit code and is only shown for a native command whose exit code is new since the previous prompt.- The duration appears only when the last command took at least
PromptMinDurationSeconds(a value of0or less falls back to 2). - Git status:
โahead,โbehind,+staged,~modified,?untracked,!conflicts,$stashes,โclean. A detached HEAD shows:abc1234. - Git status is skipped on UNC paths and, on Windows, network drives. Results are cached per directory and command (5 seconds, or 30 seconds after a timeout), and
โฆreplaces the status ifgit statusexceedsGitPromptTimeoutMs. - The
Randomscheme picks new colors on every prompt. EnableRandomTitleonly has an effect while theHackermanscheme is applied.
There are 12 color schemes (the PromptColorSchemes enum):
Default,Blue,Green,Cyan,Red,Magenta,Yellow,GrayRandom,Asturias,Spain,Hackerman
# Change the scheme for the current session only
Set-PromptColorScheme -ColorScheme "Hackerman"
# Persist it (writes the whole current settings object to powershell.config.json)
Save-UserSettingsSet-PromptColorScheme only updates the in-memory settings; the prompt re-applies the stored scheme on every redraw. To make the change permanent either run Save-UserSettings afterwards or edit powershell.config.json:
{
"Microsoft.PowerShell.Profile:PromptColorScheme": "Hackerman"
}The profile reads its settings from powershell.config.json, located in the same directory as your profile (Split-Path -Parent $PROFILE). If the file does not exist, the first start creates it with all defaults:
{
"Microsoft.PowerShell.Profile:PromptColorScheme": "Default",
"Microsoft.PowerShell.Profile:PromptEngine": "Custom",
"Microsoft.PowerShell.Profile:OhMyPoshTheme": "Themes/gruvbox.omp.json",
"Microsoft.PowerShell.Profile:DefaultPrompt": false,
"Microsoft.PowerShell.Profile:AskCreateCodeFolder": true,
"Microsoft.PowerShell.Profile:CodeFolderName": "Code",
"Microsoft.PowerShell.Profile:EnableRandomTitle": false,
"Microsoft.PowerShell.Profile:EnableMathModule": true,
"Microsoft.PowerShell.Profile:GitPrompt": true,
"Microsoft.PowerShell.Profile:GitPromptTimeoutMs": 1500,
"Microsoft.PowerShell.Profile:PromptMinDurationSeconds": 2,
"Microsoft.PowerShell.Profile:EnableZoxide": true,
"Microsoft.PowerShell.Profile:ShowSystemInfo": true,
"Microsoft.PowerShell.Profile:ShowPublicIP": false
}Missing keys fall back to these defaults, and unknown keys in the file are kept. If the file exists but is empty or invalid JSON, the profile warns and uses the defaults for that session (without overwriting the file).
This file is shared with PowerShell itself.
powershell.config.jsonin the user configuration directory is also read by PowerShell at startup for its own settings (for exampleMicrosoft.PowerShell:ExecutionPolicy,PSModulePath,ExperimentalFeatures). Keep any existing PowerShell keys when you add theMicrosoft.PowerShell.Profile:*keys. Per Microsoft's documentation, invalid JSON in that file can prevent an interactive session from starting, and PowerShell ignores keys it does not recognize.Save-UserSettingsrewrites the whole file from the settings loaded in memory.
All keys are prefixed with Microsoft.PowerShell.Profile: in the file.
| Setting | Description | Default |
|---|---|---|
PromptColorScheme |
Color scheme of the Custom prompt (one of the 12 schemes) |
"Default" |
PromptEngine |
Custom or OhMyPosh (see Prompt engines) |
"Custom" |
OhMyPoshTheme |
Theme used by the OhMyPosh engine; relative to the profile folder, or an absolute path |
"Themes/gruvbox.omp.json" |
DefaultPrompt |
Use a plain PS path> prompt instead of either engine |
false |
AskCreateCodeFolder |
Ask to create the Code folder if it is missing (only in interactive sessions with input not redirected; answering anything but Y/YES sets this to false and saves the file) |
true |
CodeFolderName |
Name of the code directory under $HOME (used by cdc) |
"Code" |
EnableRandomTitle |
Randomized window title when the Hackerman scheme is applied (Custom engine) |
false |
EnableMathModule |
Import Profile/Math.psm1 at startup |
true |
GitPrompt |
Show branch and status in the Custom prompt |
true |
GitPromptTimeoutMs |
Maximum wait for git status before showing โฆ (values <= 0 use 1500) |
1500 |
PromptMinDurationSeconds |
Show the last command duration at or above this threshold (values <= 0 use 2) |
2 |
EnableZoxide |
Run zoxide init powershell when zoxide is on PATH |
true |
ShowSystemInfo |
Print the system banner on the first profile load of each calendar day (see System banner) | true |
ShowPublicIP |
Make the banner request your public IP (network request) | false |
powershell.config.jsonandprofile.ps1are untracked (listed in.gitignore), because they hold local state.profile.ps1is not a file this loader sources: it is PowerShell's own CurrentUserAllHosts profile ($PROFILE.CurrentUserAllHosts), which PowerShell runs by itself, before the CurrentUserCurrentHost profile (Microsoft.PowerShell_profile.ps1), for every host. Because it is untracked, it is the place for settings that belong to one machine only (for example a localJAVA_HOMEor extraPATHentries), which keeps the shared profile generic. The repository works without it.- Data directory (
Get-ProfileDataDirectory, used for the cachedghcompletion script):$env:POWERSHELL_PROFILE_DATAif set; otherwise%LOCALAPPDATA%\PowerShell-Profileon Windows,$XDG_DATA_HOME/PowerShell-Profile, or~/.local/share/PowerShell-Profile. - Banner marker directory:
~/.local/share/powershell-profile/(see System banner). Note the different capitalization and location compared with the data directory on Windows. - API keys:
~/.powershell-secrets/(see Secure API Key Storage).
Microsoft.PowerShell_profile.ps1 is a short loader that dot-sources the files in Profile/ in a fixed order and records how long each took. Parts marked lazy are not read at startup.
| File | Loaded | Contents |
|---|---|---|
Settings.ps1 |
startup | Settings file handling, OS detection, Code folder prompt |
Common.ps1 |
startup | Shared helpers (fast executable lookup, data directory) |
Git.ps1, Prompt.ps1 |
startup | Git status and the prompt (including the Oh My Posh switch) |
Utilities.ps1 |
startup | Weather, IP, disk space, directory tree, Stop-ProcessConstantly |
Shortcuts.ps1 |
startup | Navigation and git shortcuts, zoxide |
SystemInfo.ps1 |
startup | Show-SystemInfo / sysinfo and the once-a-day banner |
Pip.ps1, Aliases.ps1 |
startup | pip wrappers; aliases |
LazyLoaders.ps1 |
startup | Registers stub functions and tab completers for the lazy parts below |
Math.psm1 |
startup (unless EnableMathModule is false) |
Math functions, imported as a module |
Copilot.ps1 |
lazy | ghcs, ghce |
SecureApiKey.ps1 |
lazy | API key storage |
GeminiTools.ps1, GeminiChat.ps1, GeminiCommit.ps1 |
lazy | Risk analysis and text formatting, chat, commit messages |
Completions.ps1 |
lazy | Completion helpers (Update-ProfileCompletions) and the implementations behind git, gh and cdc completion |
Diagnostics.ps1 |
lazy | Show-ProfileInfo |
Readline.ps1 |
never | Optional PSReadLine tuning (prediction mode and history filtering). It is present in the repository but is not in the loader's list, so it is not active; the PredictionMode, PredictionViewStyle and PredictionToggleKey keys it reads are not part of the defaults. |
Lazy loading, as implemented. For each lazy function the loader registers a small global stub. The first call dot-sources the real file, replaces the stub with the real function and forwards the call; later calls go straight to the real function. If the file defined no such function, the stub throws instead of recursing. Tab completion for git, gh and cdc is registered at startup by LazyLoaders.ps1; the first time you press Tab for one of them, the completer loads Completions.ps1. For gh, the first completion generates and caches the completion script by running gh completion -s powershell (this needs gh, and it is why the first gh completion is slower); Update-ProfileCompletions regenerates it after a gh upgrade. Tests/Profile.Lazy.Tests.ps1 asserts that no lazy part is loaded at startup, that the first call to representative functions loads their part, and that git completion works on the first and later attempts. Lazy loading reduces the work done at startup, but this README makes no promise about the resulting startup time or about any warning disappearing; measure your own system with Show-ProfileInfo (per-part load times for the current session).
| Command | Action |
|---|---|
.., ..., .... |
Go up one, two or three folders |
mkcd <path> |
Create a folder and enter it |
cdc [project] |
Jump to your Code folder (tab-completes projects) |
gst, gd, gds |
git status -sb, git diff, git diff --staged |
ga, gaa, gcmsg <msg> |
git add, git add --all, git commit -m |
gco, gsw, gpull, gpush |
git checkout, switch, pull, push |
glog |
Compact graph of the last 20 commits |
gundo |
Undo the last commit, keeping its changes staged |
sysinfo [-PublicIP] |
Print the system banner now (see System banner) |
Show-ProfileInfo |
PowerShell version, load time per part, tool lookup (git, gh, python, pip, docker, nvim, zoxide, openssl) |
Update-ProfileCompletions |
Regenerate the cached gh completion script (needs gh) |
gst, gd, ga and the other Git shortcuts forward any extra arguments to git. They are plain functions, so they take precedence over any external command with the same name on your PATH.
API keys (by default GeminiAPI) can be stored per user. You normally do not need to call these functions: gemini-chat and sgcm ask for the key with masked input the first time and store it.
Set-SecureApiKey -ApiKey "your-gemini-api-key" -KeyName "GeminiAPI"
$apiKey = Get-SecureApiKey -KeyName "GeminiAPI"| Platform | Method | File |
|---|---|---|
| Windows | DPAPI (ConvertFrom-SecureString), tied to your Windows account |
~/.powershell-secrets/<KeyName>.key |
| Linux | openssl enc -aes-256-cbc -pbkdf2 |
~/.powershell-secrets/<KeyName>.key |
| macOS | Keychain through security, with the OpenSSL file as fallback |
Keychain / same file |
Keys live in ~/.powershell-secrets/ (mode 700 on Linux/macOS, file 600 only for migrated keys), outside the profile folder. A <KeyName>.key found next to the profile (the old location) is moved there on first use. Be realistic about the protection offered on Linux and for the macOS fallback:
- The OpenSSL passphrase is derived from your username, hostname and key name, not from a secret you provide. It stops casual reading of the file, but anyone who can read the file and knows those values can decrypt it.
- The passphrase is passed to
opensslon its command line (-pass pass:...), so it can be visible to other local users in the process list while the command runs. Get-SecureApiKeyreturns the key as plain text and the value is sent to Google in an HTTP header by the Gemini functions.- See Known limitations for an unverified macOS Keychain issue.
Test-PowerShellCodeRisk is a keyword heuristic and not a sandbox or a security boundary. It matches the text of the code against a list of patterns (file, process, network, package-manager, privilege and system-path terms) and returns $true for a match. It can both over-match (any text containing rm, del, cp or curl, even inside a longer word such as Format-Table) and miss dangerous code.
Test-PowerShellCodeRisk "Remove-Item C:\Important -Force" # True
Test-PowerShellCodeRisk "Get-Date" # False
Test-PowerShellCodeRisk "Get-ChildItem | Format-Table" # True (false positive: "rm" in "Format")
# Returns a result object, it does not print the command's output
$r = Invoke-SafePowerShellCode "Get-Date"
$r.Output; $r.Error; $r.WasRisky; $r.ExecutedInvoke-SafePowerShellCode asks for y/yes confirmation when the heuristic flags the code (skip with -AutoApprove), runs the code as a background job, and stops it after 30 seconds. It returns ExecutionResult, Output, Error, WasRisky, UserApproved and Executed. ghcs uses the same heuristic to print a warning before it asks Run it? (y/N); it never runs a suggestion without that confirmation, and when input is not interactive the answer defaults to "no".
# Directories only (hidden/dot entries are always skipped)
Show-DirectoryTree -Path . -Depth 2
# Include files, and hide those matching .gitignore files found under the path
Show-DirectoryTree -Path . -IncludeFiles -RespectGitIgnore -AdditionalIgnore "*.log"
# Print every non-ignored file with its relative path, optionally in Markdown fences
Get-ContentRecursiveIgnore -Path . -UseMarkdownFence $true-RespectGitIgnoreonly has an effect together with-IncludeFiles; without-IncludeFilesnothing is filtered..gitignorehandling is a simplified glob matcher written for this profile, not Git itself, so complex patterns (such as**or character classes) may not behave as in Git. Usegit ls-fileswhen exactness matters.Get-ContentRecursiveIgnorealways applies ignore patterns, skips hidden/dot entries, and reads every remaining file as text, including large or binary files. Check the tree it prints first when pointing it at an unfamiliar folder.
Invoke-SuggestCommitMessage (alias sgcm) asks Gemini for a commit message based on your staged changes. It needs a Gemini API key (asked for on first use) and network access.
# Basic usage - analyzes staged changes and suggests a commit message
sgcm
# or
Invoke-SuggestCommitMessage
# Customize the number of previous commits sent as style context (default: 100)
Invoke-SuggestCommitMessage -CommitCount 50
# Use a specific Gemini model (default: the gemini-flash-latest alias)
Invoke-SuggestCommitMessage -Model "gemini-flash-latest"
# Commit with the suggestion without prompting for confirmation
Invoke-SuggestCommitMessage -Force
# Add specific instructions for the commit message
Invoke-SuggestCommitMessage -AdditionalInstructions "Use conventional commits format with emoji"
# Load instructions from a file
Invoke-SuggestCommitMessage -InstructionsFile "commit-guidelines.txt"
# Show detailed progress information
Invoke-SuggestCommitMessage -Verbose
# Get just the message for use in scripts/automation (no prompts, $null on failure)
$commitMsg = Invoke-SuggestCommitMessage -ReturnOnly
# Forget the stored API key and enter a new one
Invoke-SuggestCommitMessage -ResetApiKeyWhat it does:
- Reads the staged diff, the branch name and the last
-CommitCountcommit subjects, and sends all of them to the Gemini API (generativelanguage.googleapis.com). The whole staged diff is sent, so avoid it for content you cannot share. - Tells the model to match the language, format and prefixes of the previous commits; how well it does so depends on the model.
- Lists the staged files before acting.
- Interactive loop: type
commit(lowercase, exact) to rungit commit -mwith the message, press Enter to copy it to the clipboard, or type anything else as feedback to regenerate the message with the conversation so far. -Forcecommits immediately after printing the message and the staged files.-ReturnOnlyprints nothing and returns the message string.
Workflow:
- Stage your changes:
git add . - Run:
sgcm - Review the generated commit message
- Choose an action:
- Type
committo commit with the message - Press Enter to copy to clipboard
- Type feedback to refine the message (e.g., "make it shorter")
- Type
The profile contacts the network only when you run the following, plus one optional startup request. Nothing in this table is called by -NoProfile sessions, and none of it runs at startup unless stated.
| Command | Contacts | Sends |
|---|---|---|
sysinfo -PublicIP, or ShowPublicIP: true |
ifconfig.me, then api.ipify.org as a fallback |
an HTTPS request (your public address is visible to the service). With ShowPublicIP this happens during the startup that prints the banner |
Get-PublicIP |
api.ipify.org |
an HTTPS request |
Get-Weather |
wttr.in; also ipinfo.io when no place is given |
the place name (or your public address, for the location lookup) |
Get-ChtShHelp, Get-PowershellChtShHelp |
cht.sh |
the command name in the URL |
gemini-chat, sgcm |
generativelanguage.googleapis.com |
your prompts, or the staged diff and commit subjects; your API key as a header |
ghcs, ghce |
whatever the GitHub CLI extension contacts | your prompt (legacy, see below) |
- PowerShell 7 on Windows, Linux or macOS. The profile is written for PowerShell 7 (it relies on
$IsWindows/$IsLinux/$IsMacOS,[Random]::Sharedand thecleanblock, added in 7.3) and has not been tried on Windows PowerShell 5.1. Everything that was verified ran on 7.6.7. - Git for the prompt status, the Git shortcuts and the commit tool
Nothing below is installed or downloaded by the profile.
- Oh My Posh: only for
PromptEngine: OhMyPosh. Install it with your preferred method; see ohmyposh.dev. - zoxide: initialized when found and
EnableZoxideis notfalse. - Neovim: the
vim,viandgvimaliases point atnvim. See Aliases. - Python: only for the pip wrappers and the
[py:...]tag.
For AI features (gemini-chat, sgcm):
- A Google Gemini API key and an Internet connection.
- Linux, and macOS when the Keychain is not used: the
opensslcommand, used to encrypt the stored key. Install it with your package manager (it is installed by default on many systems).
For ghcs / ghce (legacy GitHub Copilot in the CLI):
- The GitHub CLI and a GitHub Copilot subscription.
- These functions run
gh copilot suggest -t <target> -s <file>andgh copilot explain, which is the interface of thegithub/gh-copilotextension (gh extension install github/gh-copilot). - Limitation: according to its repository, that extension was deprecated on 2025-10-25 and archived on 2025-10-30 in favor of GitHub Copilot CLI. Current
ghreleases ship a built-ingh copilotcommand that launches the newer Copilot CLI instead. This audit did not runghcs/ghceand did not verify whether thesuggest/explainsubcommands still work with yourghversion or extension, so treat them as legacy wrappers that may fail. Only the profile side was verified: the functions load lazily, ask for confirmation, and never run a suggestion without it.
What was actually exercised: Linux (Fedora 44), PowerShell 7.6.7 and Oh My Posh 28.10.0, using the bundled Tests/ scripts and isolated child processes.
Not run, so not verified: the Windows paths (DPAPI key storage, CIM-based uptime and memory, network-drive detection for Git status, Documents folder redirection), the macOS paths (Keychain, sysctl-based uptime, load and memory), real elevation for the [ADMIN] tag, and how the Gruvbox theme icons look in your font. Treat the cross-platform parts as written for those systems but unconfirmed.
These are observed behaviors of the current code, listed so they do not surprise you:
- Banner trigger: once per calendar day, on the first profile load, for any session type, as described under System banner. It is not limited to SSH or to any shortcut, and it does not skip non-interactive startups.
- Startup side effects: loading the profile creates
powershell.config.jsonif it is missing (also in non-interactive sessions), and, whenShowSystemInfoistrue, the banner marker directory and file. The data directory is only created when a lazy part such asShow-ProfileInfoor theghcompleter needs it. WMbanner row: the Linux window manager is hard-coded toNirirather than detected.- Aliases:
vim,viandgvimare defined unconditionally and point atnvim. Because aliases take precedence over executables, they hide a realvimand fail ifnvimis not installed. Readline.ps1: present but not loaded (see Profile Layout).Set-PromptColorSchemedoes not persist the choice (see Prompt Customization).pip install ... --globalinside an active environment passes--globalto pip (see the pip section).- macOS Keychain retrieval (not run on macOS):
Get-SecureApiKeycallsStart-Process ... -RedirectStandardOutputwithout a value. That fails parameter binding in PowerShell on any platform (confirmed on Linux by callingStart-Processwith the same switch), the error is swallowed, and the function falls back to the OpenSSL file, whichSet-SecureApiKeydoes not create when the Keychain write succeeds. The likely effect is that a stored key is not found and you are asked for it again. Get-PublicIPhas no timeout or error handling.
Get-UserSettings- Load configuration from JSON (creates the file with defaults if missing)Save-UserSettings- Write the in-memory settings to the JSON fileSet-PromptColorScheme- Set theCustomprompt colors for the current sessionPrompt- TheCustomprompt functionShow-SystemInfo(sysinfo) - Print the system banner;-PublicIPadds a public IP lookupShow-ProfileInfo- Profile diagnostics and per-part load timesUpdate-ProfileCompletions- Regenerate the cachedghcompletion scriptGet-ProfileDataDirectory,Find-ProfileExecutable- Internal helpers (data directory,PATHlookup)
Set-PWDClipboard(cpwd) - Copy current directory to clipboardGet-PublicIP- Retrieve public IP address (network request)Get-DiskSpace- Display drive space informationGet-Weather- Weather for a place (-Place,-Language,-MaxAttempts)Get-ChtShHelp,Get-PowershellChtShHelp- Query cht.sh for command help (the second prefixespowershell/)Stop-ProcessConstantly- Repeatedly stop processes with a given name (-ProcessName,-IntervalMilliseconds10-60000, default 250) until you press Ctrl+C
Show-DirectoryTree(tree) - Display directory tree (-Path,-Depth,-IncludeFiles,-RespectGitIgnore,-AdditionalIgnore)Get-ContentRecursiveIgnore- Get file contents with ignore patterns (-Path,-AdditionalIgnore,-UseMarkdownFence)mkcd,cdc,..,...,....- Navigation (see Shortcuts)
Exported by Profile/Math.psm1 (20 functions):
- Trigonometric:
Get-Sin,Get-Cos,Get-Tan,Get-Asin,Get-Acos,Get-Atan,Get-Atan2 - Power/Root:
Get-Sqrt,Get-Pow,Get-Exp - Logarithmic:
Get-Log,Get-Log10 - Rounding:
Get-Round,Get-Ceiling,Get-Floor,Get-Truncate - Utility:
Get-Abs,Get-Max,Get-Min,Get-Sign
Invoke-GeminiChat(gemini-chat) - Google Gemini AI chat (-InitialPrompt,-Model,-ResetApiKey)Invoke-SuggestCommitMessage(sgcm) - Commit message suggestions (-CommitCount,-Model,-Force,-AdditionalInstructions,-InstructionsFile,-ResetApiKey,-ReturnOnly)Get-DefaultGeminiFlashModel- Returns the default model name (gemini-flash-latest)Format-GeminiText- Render Gemini's[FG:Color]/[BG:Color]formatting markersTest-PowerShellCodeRisk- Keyword heuristic for risky codeInvoke-SafePowerShellCode- Run code in a job after the heuristic and a confirmationSet-SecureApiKey,Get-SecureApiKey(and the internalGet-ApiKeyFilePath,Set-SecureApiKeyUnix,Get-SecureApiKeyUnix) - API key storage
ghcs- Suggest a command (-Target gh|git|shell, defaultshell); shows it, flags risky ones and asks before runningghce- Explain a command
| Alias | Command | Description |
|---|---|---|
vim |
nvim |
Neovim (defined even if nvim is not installed) |
vi |
vim |
Resolves to nvim through vim |
gvim |
vim |
Resolves to nvim through vim |
wrh |
Write-Host |
Write to host |
cpwd |
Set-PWDClipboard |
Copy working directory |
tree |
Show-DirectoryTree |
Directory tree display (shadows an external tree command) |
gemini-chat |
Invoke-GeminiChat |
Gemini chat |
sgcm |
Invoke-SuggestCommitMessage |
Commit message suggestions |
pip |
Invoke-PipWrapper |
pip with the global-install warning |
pip3 |
Invoke-Pip3Wrapper |
pip3 with the global-install warning |
sysinfo |
Show-SystemInfo |
System banner on demand |
There is no gemini alias; use gemini-chat or Invoke-GeminiChat.
Contributions are welcome, though there is no formal process or response-time commitment:
- Fork this repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Clone the repository.
- Make your changes in
Profile/*.ps1(orThemes/).Microsoft.PowerShell_profile.ps1is only the loader; a new startup part must be added to its$ProfilePartslist, and a new lazy part needsRegister-LazyFunctionentries inProfile/LazyLoaders.ps1. - Test in a fresh
pwshsession, ideally withHOME/XDG_*pointing at a scratch directory so your real settings, banner marker and stored keys are not touched.Tests/Profile.Lazy.Tests.ps1does this for you. - Run the checks listed under Prompt engines. Only
Profile.Lazy.Tests.ps1redirectsHOMEandXDG_*; the two prompt tests do not. - Update this README if behavior or defaults change.
- Follow PowerShell best practices
- Add documentation for new functions
- Test on multiple platforms when possible
- Update the README for new features
Done:
- Modular layout: loader plus
Profile/*.ps1parts, with per-part load timings (Show-ProfileInfo) - Lazy loading of the Gemini, Copilot, API-key, completion and diagnostics parts
- Optional math module (
EnableMathModule) - Alternative prompt engine: Oh My Posh with a bundled Gruvbox theme and fallback to
Custom - System banner with
sysinfo, once per calendar day
Not done:
- Installation Script: automated cross-platform installation (the steps above are manual)
- Fully optional feature sets: only the math module can be switched off; the AI, GitHub, development-tools and security parts are always present
- Interactive-only / trigger-limited banner: guard the banner against non-interactive startups and decide which sessions should show it (see Known limitations)
- Current GitHub Copilot CLI support: replace or adapt
ghcs/ghcefor the newer Copilot CLI - Enhanced Terminal Integration: Better terminal-specific features
- Cloud Synchronization: Settings sync across machines
- Package Manager Integration: Direct tool installation
- Additional AI Providers: OpenAI, Claude, etc.
- Advanced Security: Code sandboxing and ML-based risk analysis
This is a personal configuration shared as-is, with no support or response-time commitment. If you find a problem, you can open an issue and include your OS, PowerShell version ($PSVersionTable.PSVersion) and the output of Show-ProfileInfo.
This repository does not currently contain a LICENSE file, so no license terms are declared for it.
- PowerShell: the shell this profile is written for
- Oh My Posh: optional prompt engine
- Google Gemini API: used by the optional chat and commit helpers
- GitHub CLI: used by the legacy Copilot wrappers
โญ Star this repo โข ๐ด Fork it โข ๐ Report an issue
Made with โค๏ธ by Adriรกn Cancio