Actions
Actions are keyboard shortcuts that fire when a project is selected in Odak's search panel. They open apps, open URLs, or run scripts — with variable substitution for the project's path, git state, and custom per-project values.
All actions live in a single YAML file: ~/.odak/config.yaml. Edit it in your favourite editor — Odak watches the file and reloads automatically when you save.
The Settings → Actions tab is a read-only dashboard: it shows what's loaded, surfaces validation issues, and exposes Edit config.yaml + Reload buttons. Actions are not created, edited, or deleted from the UI — the file is the single source of truth.
Quick start #
Every action has a name, a type, and a shortcut. The shortcut fires when you've selected a project in the search panel.
actions:
- name: Cursor
type: app
shortcut: cmd+c
app: Cursor
That's the whole vocabulary. Add more blocks under actions:, save the file, and Odak reloads.
Note. YAML is the native format, but JSON configs parse too (JSON is a subset of YAML). The file ships as YAML so you can add comments explaining what each action does. Legacy JSON configs migrate to YAML automatically on first load.
Field reference #
Every field at a glance. Only name, type, and shortcut are truly always required — other requirements depend on type.
Top-level
| Field | Required | Default | Applies to | Description |
|---|---|---|---|---|
name | ✅ | — | all | Display name. Also the target when referenced by on_select. |
type | ✅ | — | all | app | url | script |
shortcut | ✅ | — | all | Keyboard combo (e.g. cmd+c). Empty string means "no binding" — use that only when the action is the target of on_select. |
icon | ⬜ | type default | all | SF Symbol override. |
conditions | ⬜ | — | all | Gates when this action matches — see Conditional actions. |
type: app
| Field | Required | Default | Description |
|---|---|---|---|
app | ✅* | — | Application name. Resolved via /Applications/{name}.app, bundle-ID, or ~/Applications. |
app_path | ✅* | — | Absolute path to a .app bundle. Use only when app can't resolve. |
* Exactly one of app or app_path must be set. Prefer app — it's portable across machines.
type: url
| Field | Required | Default | Description |
|---|---|---|---|
url | ✅ | — | URL template. Supports {variables}. Opens in the default browser. |
type: script
| Field | Required | Default | Description |
|---|---|---|---|
script | ✅ | — | Script source. Supports {variables}. 10-second timeout. |
language | ⬜ | zsh | zsh | bash | applescript | jxa |
output | ⬜ | silent | silent | notification | clipboard | hud | list |
on_select | ⬜ | — | Requires output: list. Name of another action to chain with {selected} bound. |
Action types #
App
Opens the selected project folder in a macOS application.
- name: VS Code
type: app
shortcut: cmd+v
app: Visual Studio Code
Odak resolves app: by trying, in order:
/Applications/{name}.app- System applications (
/System/Applications,/System/Applications/Utilities) ~/Applications/{name}.app- NSWorkspace lookup by bundle identifier
If none match you'll see a validation issue in Settings. Fix it by setting app_path: instead:
- name: Custom Build
type: app
shortcut: cmd+shift+b
app_path: /Users/me/MyApps/CustomBuild.app
URL
Opens a URL (with variable substitution) in your default browser.
- name: GitHub Actions
type: url
shortcut: cmd+shift+a
url: "{remoteURL}/actions"
Script
Runs a shell script with the project directory as the working directory.
- name: Run Tests
type: script
shortcut: cmd+shift+t
language: zsh
output: notification
script: |
swift test 2>&1 | tail -20
Scripts run with a 10-second timeout and receive every resolved variable as $ODAK_* environment variables. Use language: to pick the runtime — defaults to zsh.
For multi-line scripts, use YAML's | (literal) or > (folded) block scalar — much nicer than escaped \ns:
- name: Smart push
type: script
shortcut: cmd+shift+u
output: notification
script: |
if [ -z "$(git log origin/{branch}..HEAD 2>/dev/null)" ]; then
echo "Nothing to push on {branch}"
else
git push -u origin {branch}
fi
Variables #
Use {variable} anywhere in a url: or script: template. Every resolved variable is also exposed as a $ODAK_* environment variable inside scripts.
Project
| Token | Resolves to |
|---|---|
{path} | Full project path — /Users/you/code/my-app |
{name} | Project folder name — my-app |
{dir} | Parent directory path |
{workspace} | First .xcworkspace in the project (if any) |
Git
| Token | Resolves to |
|---|---|
{remoteURL} | Git remote URL, normalised to HTTPS form |
{branch} | Current branch (via git rev-parse --abbrev-ref HEAD) |
{gitHash} | Short HEAD commit hash (via git rev-parse --short HEAD) |
Git variables resolve to empty strings when the project isn't a git repo — the action still runs.
Context
| Token | Resolves to |
|---|---|
{clipboard} | Current macOS clipboard contents |
{selection} | Current search query in Odak |
{selected} | Line selected from a list-output action (see chaining) |
Custom
Any key under variables: in the project's .odak file resolves as {myKey} and is injected as $ODAK_myKey.
Environment variables (scripts) #
In addition to {variable} substitution, every resolved variable is injected as $ODAK_* into the script's environment. Useful when you want to interpolate safely (no re-quoting), or when the value could contain characters that would break templated substitution.
- name: Open PR by branch
type: script
shortcut: cmd+shift+p
output: silent
script: |
# $ODAK_BRANCH and $ODAK_REMOTEURL are safer here than {branch}/{remoteURL}
gh pr view --web --repo "$ODAK_REMOTEURL" "$ODAK_BRANCH" \
|| gh pr create --web --repo "$ODAK_REMOTEURL" --head "$ODAK_BRANCH"
Naming: $ODAK_ + the uppercase variable name — {name} → $ODAK_NAME, {remoteURL} → $ODAK_REMOTEURL, custom {apiURL} → $ODAK_APIURL.
Script output modes #
| Mode | What happens |
|---|---|
silent | Run in the background. No feedback. (default) |
notification | macOS notification with the first ~200 chars of stdout. On non-zero exit, shows stderr. |
clipboard | Copy stdout to the clipboard. On non-zero exit, copies stderr. |
hud | Brief on-screen HUD with the result (disappears after ~2s). |
list | Parse stdout line-by-line into a selectable overlay. Selection can chain via on_select. |
Conditional actions #
Define the same shortcut multiple times with different conditions. The first action whose conditions match wins.
- name: Test
type: script
shortcut: cmd+shift+t
output: notification
script: swift test
conditions:
fileExists: Package.swift
- name: Test
type: script
shortcut: cmd+shift+t
output: notification
script: go test ./...
conditions:
fileExists: go.mod
- name: Test
type: script
shortcut: cmd+shift+t
output: notification
script: npm test
conditions:
fileExists: package.json
Condition fields
| Field | Type | Description |
|---|---|---|
fileExists | string | string[] | File/directory that must exist at the project root. List form = any-of. |
variables | mapping | Each entry must match a .odak variable exactly. |
Actions without a conditions field always match.
Interactive lists & chaining #
Set output: list to turn script stdout into a selectable list overlay. The user picks an item; if you set on_select: to the name of another action, that action runs with {selected} bound to the chosen line.
Switch branch
- name: Switch branch
type: script
shortcut: cmd+b
output: list
on_select: checkout-branch
script: git branch --format='%(refname:short)'
- name: checkout-branch
type: script
shortcut: "" # no direct binding — only runs via on_select
output: notification
script: git checkout {selected}
Open a pull request from a list
- name: Open PR
type: script
shortcut: cmd+p
output: list
on_select: open-pr-web
script: gh pr list --json number,title --jq '.[] | "#\(.number) \(.title)"'
- name: open-pr-web
type: script
shortcut: ""
output: silent
script: gh pr view $(echo '{selected}' | awk '{print $1}' | tr -d '#') --web
If you omit
on_select, selecting a list item copies it to the clipboard.
Per-project configuration #
Drop a .odak file at any project root to override defaults and define custom variables per-project. All fields optional.
ide: Cursor
terminal: Warp
gitGUI: Tower
variables:
apiURL: https://staging.example.com
team: platform
Now {apiURL} and {team} resolve in any action run on this project, and scripts also see $ODAK_APIURL and $ODAK_TEAM.
Condition matching can reference these too:
- name: Deploy
type: script
shortcut: cmd+d
output: hud
script: make deploy-{team}
conditions:
variables:
team: platform
Validation #
Odak validates the config on every load and surfaces issues in Settings → Actions → Configuration Issues.
Detected problems:
- Parse errors — invalid YAML with line/column when available.
- Missing required fields — e.g.,
app:missing fortype: app. - Unresolved apps — named app not found in standard locations.
- Invalid paths —
app_path:that doesn't exist or isn't an.appbundle. - Script dry-run failures — each script runs with dummy variables and a 3-second timeout to catch syntax errors early.
- Shortcut collisions — two actions bound to the same combo (first wins at runtime).
- macOS reserved shortcuts —
⌘Q,⌘H,⌘M,⌘W,⌘Tab,⌘Spacenever fire. - Unsupported language / output — with the list of valid values.
- Invalid
on_select— target action name doesn't exist, oroutput:isn'tlist. - Malformed shortcut strings — unrecognised key names.
A bad config does not break the previously-loaded state — Odak keeps running on the last-known-good config and shows the parse error until you fix the file.
Sharing & importing #
Any https:// URL that serves action YAML (or JSON — both parse) can be imported with one click via Odak's URL scheme. Host on a GitHub Gist, a raw repo file, or a pastebin:
odak://import?url=https://gist.githubusercontent.com/you/abc/raw/my-action.yaml
Inline JSON works too — handy for pasting into Slack/Discord where YAML's indentation would break:
odak://import?json={"name":"Deploy","type":"script","shortcut":"cmd+d","script":"make deploy"}
Imports:
- Accept a single action, a list of actions, or an object with an
actions:list. - Receive fresh UUIDs (no collisions with existing entries).
- Append as a YAML block at the end of your
config.yaml— existing comments and formatting are preserved. - Get validated like any other action.
- Notify you of the result.
Siri, Shortcuts, and Spotlight #
Odak exposes two App Intents:
- Open Project — "Open my-app in Cursor"
- Run Odak Action — runs a named action on a project
Both appear in Siri, Shortcuts.app, Spotlight, and Control Center. No extra setup.
Workflows #
The per-category examples below show individual actions. This section is the opposite: each vignette is a small story about how people compose actions into a larger workflow. Pick one that fits your day and steal the pattern.
1 · The oncall responder
PagerDuty fires. You have 30 seconds to get situational awareness.
You set up each service's .odak with its observability identifiers — service name, dashboard IDs, Sentry project. Your global config.yaml uses those variables to open the right dashboards for whichever project you happen to select.
# ~/.odak/config.yaml (applies to every project with the variables set)
- name: Logs (prod)
type: url
shortcut: cmd+l
url: "https://kibana.yourco.com/app/discover#/?_a=(query:(query:'service:{serviceName} AND env:prod'))"
- name: Dashboard
type: url
shortcut: cmd+shift+d
url: "https://grafana.yourco.com/d/{grafanaId}?var-service={serviceName}&from=now-1h"
- name: Recent deploys
type: url
shortcut: cmd+shift+h
url: "{remoteURL}/actions?query=workflow:deploy+branch:main"
- name: Oncall Slack channel
type: url
shortcut: cmd+shift+s
url: "https://yourco.slack.com/channels/{oncallChannel}"
# services/payments/.odak — per-service variables
variables:
serviceName: payments-api
grafanaId: abc123
oncallChannel: payments-oncall
Keystroke flow: page → Odak → type payments → select project → cmd+l (logs) → cmd+shift+d (dashboard) → cmd+shift+s (oncall channel). Four shortcuts, zero URL typing.
2 · The polyglot maintainer
You contribute to Swift packages, Go services, Node apps, and Python scripts. Memorising different test commands is mental overhead.
Bind one shortcut per action and let conditions.fileExists pick the right tool. Works the same way whether you're in Odak (Swift), api (Go), web (Node), or etl (Python).
# "Run tests" — cmd+shift+t, dispatches on project shape
- name: Test
type: script
shortcut: cmd+shift+t
output: notification
script: swift test 2>&1 | tail -30
conditions:
fileExists: Package.swift
- name: Test
type: script
shortcut: cmd+shift+t
output: notification
script: go test ./... 2>&1 | tail -30
conditions:
fileExists: go.mod
- name: Test
type: script
shortcut: cmd+shift+t
output: notification
script: npm test 2>&1 | tail -30
conditions:
fileExists: package.json
- name: Test
type: script
shortcut: cmd+shift+t
output: notification
script: pytest 2>&1 | tail -30
conditions:
fileExists: pyproject.toml
Do the same for Build, Lint, Install deps, Open package manifest. Your hands stop caring what language the project is in.
3 · The fast reviewer
You're a reviewer on 12 PRs today. The tax of "checkout, pull, open, run tests" is the whole reason you're behind.
Turn PR list into a pickable overlay; selecting one checks it out and opens it. A second shortcut runs the tests; a third approves.
- name: Review a PR
type: script
shortcut: cmd+alt+r
output: list
on_select: checkout-and-open
script: gh pr list --json number,title,author --jq '.[] | "#\(.number) — \(.title) (@\(.author.login))"'
- name: checkout-and-open
type: script
shortcut: ""
output: notification
script: |
num=$(echo '{selected}' | awk '{print $1}' | tr -d '#')
gh pr checkout "$num" && cursor .
- name: Approve current PR
type: script
shortcut: cmd+alt+a
output: notification
script: gh pr review --approve --body "LGTM ✅"
Flow: select repo → cmd+alt+r → pick PR → read → cmd+shift+t (runs the right test from workflow #2) → cmd+alt+a. One review, ~8 keypresses.
4 · The release cutter
Friday release. You bump the version, generate notes from commit history, tag, push, open the GitHub release page with the body prefilled.
- name: Cut release
type: script
shortcut: cmd+shift+v
output: hud
script: |
set -e
current=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
read -p "New version (current $current): " next < /dev/tty
notes=$(git log "$current..HEAD" --pretty=format:'- %s')
git tag -a "$next" -m "$next"
git push origin "$next"
open "{remoteURL}/releases/new?tag=$next&body=$(printf %s "$notes" | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read()))')"
cmd+shift+v, type the new version, and you're at GitHub's release page with every commit message already in the body. Edit and publish.
5 · The team onboarder
A new teammate joins. Instead of a 2-page "how to set up your mac" doc, you ship them one file.
Your team's dotfiles repo contains a config.yaml with every internal tool — Grafana, Sentry, Linear, internal admin panels, the staging console. On day one:
git clone [email protected]:yourco/dotfiles ~/dotfiles
ln -s ~/dotfiles/odak/config.yaml ~/.odak/config.yaml
The new teammate now has the same shortcuts as everyone else: cmd+shift+1 opens the staging admin, cmd+shift+2 opens Sentry, cmd+l opens Kibana filtered to whatever service they're on. Zero "hey what's the URL for…" questions in week one.
Per-company secrets (service identifiers, workspace slugs) live in per-project .odak files, so the shared config.yaml stays safe to commit publicly.
6 · The context switcher
Work project: Cursor + Slack + Linear. Side project: VS Code + Discord + GitHub.
Bind the same shortcut to different tools per project using conditions.variables. Each project's .odak sets a single context variable, and your config.yaml branches on it.
# ~/.odak/config.yaml
- name: IDE
type: app
shortcut: cmd+enter
app: Cursor
conditions:
variables:
context: work
- name: IDE
type: app
shortcut: cmd+enter
app: Visual Studio Code
conditions:
variables:
context: personal
- name: Chat
type: url
shortcut: cmd+shift+c
url: "https://yourco.slack.com/channels/{slackChannel}"
conditions:
variables:
context: work
- name: Chat
type: url
shortcut: cmd+shift+c
url: "https://discord.com/channels/{discordGuild}/{discordChannel}"
conditions:
variables:
context: personal
# ~/code/work-project/.odak
variables:
context: work
slackChannel: platform-team
# ~/code/side-project/.odak
variables:
context: personal
discordGuild: "123"
discordChannel: "456"
Your fingers don't have to know which world they're in.
7 · The learn-a-new-repo ritual
You cloned a big unfamiliar repo. Before reading a line of code, you want a map.
- name: Hot files
type: script
shortcut: cmd+alt+1
output: list
on_select: open-selected-file
script: |
git log --pretty=format: --name-only --since='90 days ago' \
| grep -v '^$' | sort | uniq -c | sort -rn | head -20 \
| awk '{print $2 " (" $1 " changes)"}'
- name: Top contributors
type: script
shortcut: cmd+alt+2
output: hud
script: git shortlog -sn --no-merges | head -5
- name: Recent commits
type: script
shortcut: cmd+alt+3
output: list
on_select: show-commit
script: git log --pretty=format:'%h %ad %s' --date=short -30
- name: show-commit
type: script
shortcut: ""
output: silent
script: |
hash=$(echo '{selected}' | awk '{print $1}')
open "{remoteURL}/commit/$hash"
- name: open-selected-file
type: script
shortcut: ""
output: silent
script: cursor "{path}/$(echo '{selected}' | awk '{print $1}')"
Three shortcuts tell you: which files churn the most (start there), who to ask (the top contributors), and what's happening now (recent commits). 90 seconds from clone to oriented.
8 · The speed demon
You're in a flow state. A teammate drops a GitHub issue URL in Slack. You copy it and keep moving.
- name: Open from clipboard
type: url
shortcut: cmd+shift+o
url: "{clipboard}"
- name: Branch from clipboard issue
type: script
shortcut: cmd+shift+b
output: notification
script: |
# Expects a GitHub issue URL in the clipboard
num=$(echo "{clipboard}" | grep -oE 'issues/[0-9]+' | cut -d/ -f2)
slug=$(gh issue view "$num" --json title --jq .title \
| tr '[:upper:] ' '[:lower:]-' | tr -cd 'a-z0-9-' | cut -c1-40)
git checkout -b "issue-$num-$slug"
Flow: copy the URL → select the right repo in Odak → cmd+shift+b. You're on a new branch named after the issue title, ready to code. No tabs opened, no context lost.
Examples #
Drop-in YAML you can paste into ~/.odak/config.yaml, grouped by category.
Opening apps
- name: Cursor
type: app
shortcut: cmd+c
app: Cursor
- name: VS Code
type: app
shortcut: cmd+v
app: Visual Studio Code
- name: Xcode
type: app
shortcut: cmd+x
app: Xcode
conditions:
fileExists:
- Package.swift
- "*.xcodeproj"
- name: Terminal
type: app
shortcut: cmd+enter
app: Terminal
- name: iTerm
type: app
shortcut: opt+enter
app: iTerm
- name: Fork
type: app
shortcut: shift+enter
app: Fork
Git & GitHub
- name: Open on GitHub
type: url
shortcut: cmd+g
url: "{remoteURL}"
- name: Open branch on GitHub
type: url
shortcut: cmd+shift+g
url: "{remoteURL}/tree/{branch}"
- name: Open commit
type: url
shortcut: cmd+shift+h
url: "{remoteURL}/commit/{gitHash}"
- name: Open PRs
type: url
shortcut: cmd+shift+p
url: "{remoteURL}/pulls"
- name: Open Actions runs
type: url
shortcut: cmd+shift+a
url: "{remoteURL}/actions?query=branch:{branch}"
- name: Compare branch to main
type: url
shortcut: cmd+shift+c
url: "{remoteURL}/compare/main...{branch}"
- name: Copy branch name
type: script
shortcut: cmd+shift+b
output: clipboard
script: printf "{branch}"
- name: Push current branch
type: script
shortcut: cmd+shift+u
output: notification
script: git push -u origin {branch}
- name: Pull with rebase
type: script
shortcut: cmd+shift+y
output: notification
script: git pull --rebase
Testing & linting
- name: Test
type: script
shortcut: cmd+shift+t
output: notification
script: swift test 2>&1 | tail -30
conditions:
fileExists: Package.swift
- name: Test
type: script
shortcut: cmd+shift+t
output: notification
script: go test ./... 2>&1 | tail -30
conditions:
fileExists: go.mod
- name: Test
type: script
shortcut: cmd+shift+t
output: notification
script: npm test 2>&1 | tail -30
conditions:
fileExists: package.json
- name: Test
type: script
shortcut: cmd+shift+t
output: notification
script: pytest 2>&1 | tail -30
conditions:
fileExists:
- pytest.ini
- pyproject.toml
- name: Test
type: script
shortcut: cmd+shift+t
output: notification
script: cargo test 2>&1 | tail -30
conditions:
fileExists: Cargo.toml
- name: Lint
type: script
shortcut: cmd+shift+l
output: notification
script: swiftlint --quiet 2>&1 | tail -20
conditions:
fileExists: .swiftlint.yml
- name: Lint
type: script
shortcut: cmd+shift+l
output: notification
script: eslint . 2>&1 | tail -20
conditions:
fileExists: .eslintrc.json
Build & run
- name: Build
type: script
shortcut: cmd+b
output: notification
script: xcodebuild -scheme {name} build 2>&1 | tail -20
- name: Clean build
type: script
shortcut: cmd+shift+k
output: notification
script: xcodebuild -scheme {name} clean build 2>&1 | tail -20
- name: Install deps
type: script
shortcut: cmd+i
output: notification
script: npm install
conditions:
fileExists: package.json
- name: Install deps
type: script
shortcut: cmd+i
output: notification
script: go mod download
conditions:
fileExists: go.mod
CI/CD & deploys
- name: Deploy staging
type: script
shortcut: cmd+shift+d
output: hud
script: make deploy-staging
- name: Deploy prod
type: script
shortcut: cmd+ctrl+d
output: hud
script: make deploy-prod
- name: Trigger GitHub workflow
type: script
shortcut: cmd+w
output: notification
script: gh workflow run deploy.yml --ref {branch}
Observability
Per-project variables in .odak make these reusable across services:
- name: Datadog
type: url
shortcut: cmd+shift+1
url: "https://app.datadoghq.com/dashboard/{dashboardId}?tpl_var_service={serviceName}&tpl_var_env=prod"
- name: Grafana
type: url
shortcut: cmd+shift+2
url: "https://grafana.yourco.com/d/{grafanaId}?var-service={serviceName}"
- name: Kibana logs
type: url
shortcut: cmd+l
url: "https://kibana.yourco.com/app/discover#/?_a=(query:(query:'service:{serviceName} AND env:prod'))"
- name: Sentry
type: url
shortcut: cmd+shift+e
url: "https://sentry.io/organizations/yourco/issues/?project={sentryProject}"
Team collaboration
- name: Copy PR link
type: script
shortcut: cmd+alt+p
output: clipboard
script: gh pr view --json url --jq .url | tr -d '\n'
- name: Copy Slack snippet
type: script
shortcut: cmd+alt+s
output: clipboard
script: |
printf '%s\n[%s] %s' "{remoteURL}/tree/{branch}" "{name}" "{branch}"
- name: Search Linear
type: url
shortcut: cmd+alt+l
url: "https://linear.app/your-team/search?q={selection}"
Project navigation
- name: Reveal in Finder
type: script
shortcut: cmd+f
output: silent
script: open {path}
- name: Copy project path
type: script
shortcut: cmd+alt+c
output: clipboard
script: printf '{path}'
- name: Open README
type: script
shortcut: cmd+r
output: silent
script: open README.md
- name: Project size
type: script
shortcut: cmd+alt+z
output: hud
script: du -sh {path} | awk '{print $1}'
- name: Recent files
type: script
shortcut: cmd+alt+r
output: list
script: find {path} -type f -mtime -7 -not -path '*/.*' | head -20
Interactive workflows
Scripts that output a selectable list — pair with on_select to chain the next step.
- name: Switch branch
type: script
shortcut: cmd+b
output: list
on_select: checkout-branch
script: git branch --format='%(refname:short)'
- name: checkout-branch
type: script
shortcut: ""
output: notification
script: git checkout {selected}
- name: Run npm script
type: script
shortcut: cmd+alt+n
output: list
on_select: exec-npm-script
script: jq -r '.scripts | keys[]' package.json
conditions:
fileExists: package.json
- name: exec-npm-script
type: script
shortcut: ""
output: hud
script: npm run {selected}
- name: Make target
type: script
shortcut: cmd+alt+m
output: list
on_select: exec-make
script: grep -E '^[a-zA-Z_-]+:' Makefile | sed 's/:.*//'
conditions:
fileExists: Makefile
- name: exec-make
type: script
shortcut: ""
output: hud
script: make {selected}
- name: Recent commits
type: script
shortcut: cmd+alt+h
output: list
on_select: copy-commit-hash
script: git log --pretty=format:'%h %s' -20
- name: copy-commit-hash
type: script
shortcut: ""
output: clipboard
script: printf %s "{selected}" | awk '{print $1}' | tr -d '\n'
Language-specific
Use conditions.fileExists to bind the same shortcut to the right tool per project.
- name: Open Package Manager
type: script
shortcut: cmd+alt+p
output: silent
script: open Package.swift
conditions:
fileExists: Package.swift
- name: Open Package Manager
type: script
shortcut: cmd+alt+p
output: silent
script: open package.json
conditions:
fileExists: package.json
- name: Open Package Manager
type: script
shortcut: cmd+alt+p
output: silent
script: open Cargo.toml
conditions:
fileExists: Cargo.toml
- name: Open Package Manager
type: script
shortcut: cmd+alt+p
output: silent
script: open go.mod
conditions:
fileExists: go.mod
- name: Open Package Manager
type: script
shortcut: cmd+alt+p
output: silent
script: open pyproject.toml
conditions:
fileExists:
- pyproject.toml
- requirements.txt
Schema reference #
Full action schema in YAML (every field; comments annotate requirements):
- name: string # required — display name + on_select target
type: app | url | script # required
shortcut: cmd+c # required — "" = no binding (chain-only target)
# Optional (all types)
icon: terminal # SF Symbol name override
conditions: # first matching action wins
fileExists: # string or list of strings
- Package.swift
- go.mod
variables:
team: platform # all must match .odak variables
# type: app — exactly one of `app` or `app_path`
app: Cursor # name — preferred, portable
app_path: /Applications/Cursor.app # absolute path
# type: url
url: "{remoteURL}/actions"
# type: script
script: swift test
language: zsh # zsh (default) | bash | applescript | jxa
output: notification # silent (default) | notification | clipboard | hud | list
on_select: checkout-branch # only valid with output: list
Root object:
version: 1 # optional — reserved for future migrations
actions: # required
- …
Shortcut syntax #
Human-readable and case-insensitive: cmd+c, opt+enter, cmd+shift+t, ctrl+space.
Modifiers: cmd, opt (alias: alt), ctrl, shift.
Keys: a–z, 0–9, return/enter, tab, space, escape, delete, left/right/up/down, f1–f19.
Reserved by macOS (never fire): cmd+q, cmd+h, cmd+m, cmd+w, cmd+tab, cmd+space.
FAQ #
How do I back up my actions? The whole config is a single file — cp ~/.odak/config.yaml ~/my-actions.yaml.bak.
Can I share one action with a teammate? Yes — paste the YAML block into Slack/GitHub and they paste it into their file, or use odak://import?json=… for one-click install.
Why aren't my changes picking up? Check Settings → Actions → Configuration Issues. A parse error blocks the reload until you fix the syntax. Odak keeps running on the last-known-good config so you can keep working while you fix the file.
Does .odak need to exist for a project? No — it's purely for overrides and custom variables. Actions work fine without it.
My shortcut doesn't fire. Three common causes: (1) it's macOS-reserved (see above); (2) another action has the same combo and its conditions match first; (3) the app window isn't focused — Odak shortcuts only fire while the search panel is active.
Why no GUI to add actions? By design. Odak is for developers; editing a single YAML file is faster than a form, portable across machines via a dotfile repo, and easier to share. The Settings panel still shows what's loaded and surfaces validation issues.