Hooks allow you to run custom scripts at various points during gogws command execution. They follow a git-like convention with executable files discovered automatically.
Hooks can be defined at two levels:
Located in ~/.gws/hooks/. These hooks are always trusted and run automatically.
~/.gws/
└── hooks/
├── pre-init
├── post-init
├── pre-update
└── ...
Located in <workspace>/.gws/hooks/. These hooks require trust verification before execution.
<workspace>/
└── .gws/
└── hooks/
├── pre-init
├── post-init
└── ...
Local hooks override global hooks. If a hook exists in both locations, only the local hook will be executed.
| Hook | Trigger | Description |
|---|---|---|
pre-init |
Before init command | Before workspace initialization |
post-init |
After init command | After workspace initialization |
pre-update |
Before update command | Before cloning missing repos |
post-update |
After update command | After cloning missing repos |
pre-clone |
Before cloning a repository | Before each individual clone |
post-clone |
After cloning a repository | After each individual clone |
pre-fetch |
Before fetch command | Before fetching all repos |
post-fetch |
After fetch command | After fetching all repos |
pre-ff |
Before fast-forward pull | Before pulling all repos |
post-ff |
After fast-forward pull | After pulling all repos |
pre-check |
Before check command | Before workspace check |
post-check |
After check command | After workspace check |
Local hooks from external/cloned workspaces are not automatically trusted for security reasons.
Use the --trust-hooks flag to control behavior:
| Mode | Description |
|---|---|
ask (default) |
Prompt for each untrusted hook with options to run, skip, or trust |
all |
Run all hooks without prompting |
skip |
Skip all untrusted local hooks |
Example:
gogws update --trust-hooks=skip
gogws fetch --trust-hooks=allAdd workspace paths to your trusted list in ~/.gws/config.yaml:
use-agent: true
trusted-workspaces:
- "/home/user/work/*" # All direct children of /home/user/work
- "/home/user/personal/**" # All descendants of /home/user/personal
- "/home/user/myproject" # Specific workspace pathWildcard patterns:
*- matches any single directory level**- matches any number of directory levels (recursive)
When --trust-hooks=ask (default) and a local hook is found in an untrusted workspace:
[hook:local] Hook 'pre-update' found at: /path/to/workspace/.gws/hooks/pre-update
Workspace: /path/to/workspace
This workspace is not in your trusted list.
Options:
[r] Run this hook
[s] Skip this hook
[t] Run and add workspace to trusted list
Choose [r/s/t]:
All hooks receive these environment variables:
| Variable | Description |
|---|---|
GOGWS_COMMAND |
The command being executed (init, update, clone, fetch, ff, check) |
GOGWS_WORKSPACE |
The absolute path to the workspace root directory |
GOGWS_HOOK_NAME |
The name of the hook being executed |
GOGWS_HOOK_ORIGIN |
The origin of the hook (global or local) |
- Hooks run synchronously - the command waits for the hook to complete
- Hooks run in the workspace root directory
- Hooks inherit the current environment plus gogws-specific variables
- Hook stdout/stderr are passed through to the terminal
- If a hook exits with non-zero, the parent command fails with an error
- If a hook file doesn't exist, it is silently skipped
- Hook origin is displayed:
[hook:global]or[hook:local]or[hook:local:trusted]
- Create the hooks directory:
# Global hooks
mkdir -p ~/.gws/hooks
# Local hooks (workspace-level)
mkdir -p .gws/hooks- Create your hook script (no extension needed, git-style):
#!/bin/bash
echo "Running $GOGWS_COMMAND in $GOGWS_WORKSPACE"
echo "Hook: $GOGWS_HOOK_NAME (origin: $GOGWS_HOOK_ORIGIN)"
# Example: Send notification after update
if [ "$GOGWS_COMMAND" = "update" ]; then
echo "Workspace updated!"
fi- Make it executable:
chmod +x ~/.gws/hooks/post-update
# or
chmod +x .gws/hooks/post-updateConfiguration files can now be placed in the .gws/ directory:
<workspace>/
├── .gws/
│ ├── hooks/ # Hook scripts
│ ├── projects.gws # Projects definition (preferred)
│ └── workspaces.gws # Workspaces definition (preferred)
├── .projects.gws # Legacy location (warning if both exist)
└── .workspaces.gws # Legacy location (warning if both exist)
If both .gws/projects.gws and .projects.gws exist, the .gws/ version takes priority and a warning is displayed asking to remove the legacy file.
Send a Slack notification when repositories are cloned:
#!/bin/bash
# ~/.gws/hooks/post-update
if [ -n "$SLACK_WEBHOOK" ]; then
curl -s -X POST "$SLACK_WEBHOOK" \
-H 'Content-type: application/json' \
-d "{
\"text\": \"Workspace updated\",
\"blocks\": [
{
\"type\": \"section\",
\"text\": {
\"type\": \"mrkdwn\",
\"text\": \"*Workspace updated*\n$GOGWS_WORKSPACE\"
}
}
]
}"
fiEnsure VPN is connected before fetching from private repositories:
#!/bin/bash
# .gws/hooks/pre-fetch
INTERNAL_HOST="git.internal.company.com"
if ! ping -c1 -W2 "$INTERNAL_HOST" &>/dev/null; then
echo "ERROR: Cannot reach $INTERNAL_HOST"
echo "Please connect to VPN before fetching."
exit 1
fi
echo "VPN connection verified"Automatically install dependencies for cloned repositories:
#!/bin/bash
# .gws/hooks/post-clone
echo "Installing dependencies for cloned repositories..."
for dir in */; do
[ -d "$dir/.git" ] || continue
if [ -f "$dir/package.json" ]; then
echo "→ npm install in $dir"
(cd "$dir" && npm ci --silent)
elif [ -f "$dir/go.mod" ]; then
echo "→ go mod download in $dir"
(cd "$dir" && go mod download)
elif [ -f "$dir/requirements.txt" ]; then
echo "→ pip install in $dir"
(cd "$dir" && pip install -q -r requirements.txt)
elif [ -f "$dir/Gemfile" ]; then
echo "→ bundle install in $dir"
(cd "$dir" && bundle install --quiet)
fi
done
echo "Dependencies installed"Run project-specific setup scripts after cloning:
#!/bin/bash
# .gws/hooks/post-update
for dir in */; do
[ -d "$dir/.git" ] || continue
if [ -x "$dir/scripts/setup.sh" ]; then
echo "Running setup for $dir"
(cd "$dir" && ./scripts/setup.sh)
fi
doneWarn before pulling if there are uncommitted changes:
#!/bin/bash
# ~/.gws/hooks/pre-ff
DIRTY_REPOS=""
for dir in */; do
[ -d "$dir/.git" ] || continue
if ! git -C "$dir" diff --quiet 2>/dev/null; then
DIRTY_REPOS="$DIRTY_REPOS - $dir\n"
fi
done
if [ -n "$DIRTY_REPOS" ]; then
echo "WARNING: The following repositories have uncommitted changes:"
echo -e "$DIRTY_REPOS"
echo "These will NOT be pulled to avoid conflicts."
fi-
Check file is executable:
ls -la .gws/hooks/ chmod +x .gws/hooks/your-hook
-
Check hook name matches exactly: Hook names must match exactly (no extension):
- ✅
pre-fetch - ❌
pre-fetch.sh - ❌
pre_fetch
- ✅
-
Run with verbose mode:
gogws fetch --verbose
This shows hook discovery and execution.
-
Check shebang line: Ensure the first line is a valid interpreter:
#!/bin/bash # or #!/usr/bin/env bash
Error: permission denied: .gws/hooks/pre-fetch
Solution:
chmod +x .gws/hooks/pre-fetchOn Windows, ensure the file has execute permissions or use a .bat/.ps1 extension.
"Workspace is not in your trusted list"
Options:
- One-time run: Choose
[r]at the prompt - Permanently trust: Choose
[t]to add to trusted list - Skip hooks: Use
--trust-hooks=skip - Add to config: Edit
~/.gws/config.yaml:trusted-workspaces: - "/path/to/workspace"
Bypass for CI/CD:
gogws update --trust-hooks=allIf a hook exits with non-zero, the parent command fails:
[hook:local] Running pre-fetch...
ERROR: VPN not connected
Error: pre-fetch hook failed: exit status 1
To continue despite hook failures, modify your hook to return 0:
#!/bin/bash
# Non-blocking hook
do_something || echo "Warning: do_something failed"
exit 0 # Always succeedAdd debug output to your hook:
#!/bin/bash
set -x # Print commands as they execute
echo "GOGWS_COMMAND: $GOGWS_COMMAND"
echo "GOGWS_WORKSPACE: $GOGWS_WORKSPACE"
echo "GOGWS_HOOK_NAME: $GOGWS_HOOK_NAME"
echo "GOGWS_HOOK_ORIGIN: $GOGWS_HOOK_ORIGIN"
echo "PWD: $(pwd)"
# Your hook logic here