Script scanning
Buttons discovers runnable scripts by scanning the directories you choose, then renders them as buttons. It never writes to those files. It only reads them and stores references in .buttons.json.
Scan directories
Section titled “Scan directories”The project root is always scanned at its top level. To scan elsewhere, add directories in the Project scripts tab. Each row in the Scan directories card has a recursive toggle:
- Off (default): only the directory’s top level is scanned.
- On: the directory’s whole tree is scanned.
project/├── package.json <- always found (root)├── Cargo.toml <- always found (root)├── go.mod <- always found (root)├── deploy.sh <- always found (root)├── scripts/│ ├── migrate.sh <- found when "scripts" is a scan directory│ └── jobs/seed.sh <- found when "scripts" is recursive└── services/api/ ├── package.json <- found when "services/api" is a scan directory └── main.py <- sameThe list is stored in the buttons.scanDirectories workspace setting, so it is shared with the project. Type or paste a path into the Add field: relative paths (like scripts) resolve inside the project, full paths (like /opt/tools) reach outside it. Removing a directory stops scanning it. Hidden directories and ignore-listed names (like build or node_modules) cannot be used as relative scan targets.
2.0 change: earlier versions scanned the whole workspace recursively. Nested packages are now found only when their parent directory is added as a (recursive) scan directory. Existing buttons for files outside the scan scope are kept and marked not found until you add the directory.
Outside the project
Section titled “Outside the project”Scan directories can live outside the workspace: paste a full path (/opt/tools or C:\tools). Those directories are scanned and watched like in-project ones, and their buttons run with the terminal set to the script’s own directory. Inside the project, relative paths are the rule; outside it, full paths are required.
You can also add a single file without scanning its folder:
- right-click a
.shor Python entry file in the Explorer and choose Add to Buttons, or - paste the file’s path into the Add field (full path when it lives outside the project).
Standalone file entries are stored in .buttons.json, so they travel with the project file, and they keep working even when their directory is not a scan scope. Right-clicking a manifest (package.json, Makefile, justfile, composer.json, Cargo.toml, go.mod) adds its folder as a scan directory instead, since its scripts come from scanning.
What is discovered
Section titled “What is discovered”| Kind | Found where | Run as |
|---|---|---|
package.json scripts |
any scan scope | npm run dev / pnpm dev / yarn test / bun dev |
Cargo.toml |
any scan scope | cargo build / cargo test / cargo run |
go.mod |
any scan scope | go build / go test / go run . |
Makefile targets |
any scan scope | make build |
composer.json scripts |
any scan scope | composer test |
justfile recipes |
any scan scope | just build |
.sh files |
any scan scope | bash deploy.sh |
Python entry files (app.py, main.py, manage.py, run.py, server.py) |
any scan scope | python main.py |
Every command runs with the terminal’s working directory set to the script file’s directory, and file paths in the command are relative to that directory. So scripts/migrate.sh runs as bash migrate.sh inside scripts/.
The file types above are controlled by the buttons.scriptFiles setting; package.json, shell, python, Cargo.toml, and go.mod are enabled by default, Makefile / composer.json / justfile are opt-in.
For package.json, the script body (and for Makefile/justfile, a preceding # comment) is captured as a description shown with the button. Cargo.toml and go.mod offer a fixed build/test/run set rather than parsing every target. Other ecosystems (Python pyproject.toml, .NET, and so on) are still not auto-detected. Add their commands as custom commands instead.
Disabling a file type stops offering its scripts in the Project scripts tab, but never removes buttons already in your .buttons.json.
Virtual environments
Section titled “Virtual environments”When a venv/ or .venv/ directory exists in the project root or the top level of a scan directory, Buttons offers three buttons for it (grouped under the venv’s path):
| Button | Command |
|---|---|
| Activate venv | source venv/bin/activate (or & .venv\Scripts\Activate.ps1 / .venv\Scripts\activate.bat on Windows, whichever exists) |
| Deactivate | deactivate. Runs in a terminal where the venv was activated |
| Install requirements | venv/bin/pip install -r requirements.txt (venv\Scripts\pip.exe on Windows). Uses the venv’s own pip, so it installs into the venv even before activation. Only offered when a requirements.txt sits next to the venv. |
Like all commands, venv commands run from the venv’s parent directory. A venv at packages/api/.venv activates as source .venv/bin/activate from packages/api/.
The venv’s internals are never scanned. Creating a venv is picked up on the next Rescan (or file change). On Windows, running Activate.ps1 may require a permissive PowerShell execution policy.
Package-manager detection
Section titled “Package-manager detection”The runner used for package.json scripts is detected from the root lockfiles, in this order:
| Lockfile | Package manager |
|---|---|
pnpm-lock.yaml |
pnpm |
yarn.lock |
yarn |
bun.lockb |
bun |
package-lock.json |
npm |
If none is found, it defaults to npm. The detected manager applies to all package.json files in the workspace (monorepos are assumed to use one manager). Because script entries are references, switching lockfiles and rescanning updates every command automatically. npm run dev becomes pnpm dev, and so on.
Makefile, composer.json, justfile, Cargo.toml, and go.mod always use their fixed runner (make, composer, just, cargo, go) regardless of the detected package manager.
Monorepos
Section titled “Monorepos”Add your package roots as recursive scan directories:
{ "buttons.scanDirectories": [{ "path": "packages", "recursive": true }]}Each script’s packageDir is its file’s directory, and running it opens a terminal with the working directory set there. So pnpm dev in packages/web and cargo run in crates/cli run in the right place. See Monorepos for the full walkthrough.
Ignored directories
Section titled “Ignored directories”Inside recursive scan directories, these directories are never entered (installed packages, VCS, and build/cache output):
node_modules, .git, .hg, .svn, .next, .nuxt, .output, .cache, .turbo, .yarn, .pnpm-store, dist, build, out, coverage, vendor, .venv, venv, __pycache__, .vscode, .idea, target, .svelte-kit, .parcel-cache
In addition, any hidden (dot-prefixed) directory is skipped (which is why .venv must be detected explicitly rather than scanned). Directories with these names cannot be used as scan targets.
The scan is capped at 5000 matching files per scope. Overlapping scopes are fine. A file found twice is listed once.
Generate vs Rescan
Section titled “Generate vs Rescan”Two related but distinct actions:
- Generate: first time, creates
.buttons.jsonwith every root-level discovered script. If the file already exists, adds any missing root-level scripts. Custom commands, notes, variants, and extra opted-in scripts stay. - Rescan: re-runs the scanner against the existing file. It:
- keeps your included scripts and notes,
- recomputes their commands (for example after a package-manager change),
- marks scripts that no longer exist as not found,
- leaves newly-discovered scripts unchecked for you to opt in,
- never touches custom command entries.
In short: Generate fills in root-level scripts; Rescan updates commands in place without changing what you have selected.
Checkbox selection
Section titled “Checkbox selection”In the Project scripts tab, check or uncheck scripts to include or remove them as buttons. Changes are saved immediately once the file exists.