Generate code, run host checks
A host script publishes request.txt with a leading /act and a change request. The CLI starts a background session. On end, the host runs its native compiler or test runner.
Connect a local VisiDog CLI project to your host tools using files and native filesystem events.
This integration has no polling. It requires both FSA and a working native FileSystemObserver, plus Web Locks. FSA support alone is insufficient. If observation is unavailable or refused, activation fails visibly instead of starting a periodic scan. The hook icon is absent in OPFS projects.
The observer watches only .visidog/hooks/. Activation and reactivation do not scan the input directory or admit files already present, including after reopening the browser or reconnecting FSA. Only native events identifying an input file trigger its admission; ambiguous events and directory-only notifications do not trigger a scan. A short, event-triggered delay groups duplicate notifications and lets writers finish. Switching to OPFS or choosing a different folder disables the integration for that project.
project/
└── .visidog/hooks/
├── config.json optional, shared by all project sessions
├── input/ host → CLI: UTF-8 prompt files
├── events/ CLI → host: session status snapshots
├── commands/ CLI → host: req-ID.json native commands
└── receipts/ admission records and host res-ID.json results
To install the executor on your machine, follow the Host Commands guide: complete downloadable runner, Windows and Linux setup, JSON protocol and troubleshooting.
The AI tool execute_host_command is listed only when the project uses FSA and its hooks are active. VisiDog CLI relays files: it publishes a request, waits for a native filesystem event, reads the matching result and returns it to the AI. Your host integration executes the command. Plan Mode refuses this tool.
Tool arguments: command is required; cwd defaults to . (project root), and timeout bounds browser waiting in milliseconds (default 60000, range 1–600000). Set Host command shell in CLI Settings: it is initially suggested from the browser platform (cmd.exe for Windows, /bin/sh for Linux/macOS) and must match an installed host executable. The CLI adds the saved shell value to the JSON request; the AI is informed of it and cannot override it. The host owns process execution policy.
The CLI writes commands/req-ID.json, using a generated UUID:
{
"version": 1,
"id": "12345678-1234-4234-abcd-123456789abc",
"projectId": "project-id",
"sessionId": "session-id",
"command": "npm test",
"cwd": ".",
"shell": "cmd.exe",
"timeout": 60000,
"createdAt": 1791288000000
}
The host watches commands/req-*.json, executes each request once, and publishes receipts/res-ID.json with the same ID. Publish the complete result using an atomic rename from a temporary file. Required fields are id, exitCode (integer or null), stdout and stderr (strings). An optional error string describes a host execution failure:
{
"id": "12345678-1234-4234-abcd-123456789abc",
"exitCode": 0,
"stdout": "All tests passed\n",
"stderr": ""
}
The tool returns the receipt fields with ok true only for exit code zero without a host error. Receipts are limited to 4 MiB. Duplicate events are grouped; unrelated receipts are ignored. A partial JSON write waits for another event, without polling. A valid JSON object with invalid fields fails the pending call.
Timeout, Stop, session deletion or disabling hooks ends browser waiting. It does not terminate a native process or remove its request. Commands and receipts remain on disk; the CLI never automatically resubmits a command or recovers pending waits after reload. The host must deduplicate IDs and reconcile retained requests before executing them after a restart. allowedCommands controls slash commands, not native shell requests.
Any directly contained file with a dot in its name is eligible, regardless of its extension. Subdirectories are not consumed. Temporary extensions .tmp, .part, and .swp are ignored by default.
A filename without a session-ID prefix, such as request.txt, creates a new session with a generated ID in this project, without replacing the session on screen. To continue an existing session, name the file ID_SESSION-unique-request.txt. If the filename supplies a session ID but no session with that ID exists, the CLI creates a session using the supplied ID. An ID already belonging to another project is refused.
To obtain the ID of the session currently displayed in VisiDog CLI, enter /session-id. It prints the full ID as plain text, for example session_12345678-1234-1234-abcd-123456789abc. Copy that value into a filename such as session_12345678-1234-1234-abcd-123456789abc-build-result.txt to continue the same conversation. The command is also listed in /help and returns the target session's ID when executed inside a hook.
CLI IDs use session_<UUID>; the full UUID, including its hyphens, is recognized. For custom IDs, the longest known ID prefix wins. An unknown custom ID without hyphens uses the part before the first hyphen. Use a UUID or an ID without hyphens when creating a session from the host.
/model my-configured-model-id
/plan
Plan the implementation of the validation described in requirements.md.
In the documentation, explain this literal example:
/this line belongs to the prompt
Then verify the resulting behavior.
/act
/git status
This example first asks the AI to produce a plan in Plan mode. Once that cycle finishes, the final /act approves the plan and starts its implementation. The hook waits for that execution to finish before running /git status. Returning to /plan at the end is optional; it is not required to finalize a request.
The consecutive slash lines starting at the very first line are executed before the prompt. Consecutive slash lines at the end are executed after the complete AI cycle, including its tool continuations. The internal slash line above remains prompt text. Commands at either edge are excluded from the AI prompt. Windows CRLF, UTF-8 BOM and a final newline are supported. A file containing only slash lines executes them as leading commands, without adding a prompt.
A leading blank line ends the leading command block. Trailing blank lines are treated as terminators before detecting final commands. To keep a literal slash line at the end of the prompt, follow it with a normal text line.
Commands run through the existing CLI router, bound to the target session and its filesystem. Examples include /session-id, /model, /plan, /act, /git, /config, /budget, /batch, /swarm, and /test. Leading /act enables Act mode when there is no existing plan; in a resumed session it can approve and execute an existing plan before the new prompt.
Commands that navigate projects, manage sessions, open editors, export downloads, manipulate custom buttons, clear the visible console, or reset the installation are refused. In particular: /new, /session, /fork, /folder, /reset, /button, /buttons, /edit, /merge, /export, and /clear, including their aliases. Actions that require unsupported UI handling produce HOOK_COMMAND_UNSUPPORTED. A failed command or refused AI prompt stops the request; final commands run after a successful cycle.
Every AI session in an enabled project, including sessions started manually, writes events/ID_SESSION-status.txt. Each transition replaces the previous contents:
1791203696 - 2026-10-05 12:34:56 - run
| Status | Meaning |
|---|---|
start | The first AI inference of a cycle is about to be dispatched. |
run | Another AI inference is dispatched in the same cycle, for example after tool results. |
end | The cycle closes, including completion, error or cancellation. Goal loops close after the entire loop finishes. |
The Unix timestamp is in seconds. The readable date is local browser time by default, or UTC with timestampZone. Deleting a session removes its status file even when hooks are disabled. end describes lifecycle completion, not test success; it can precede final slash commands.
A host script publishes request.txt with a leading /act and a change request. The CLI starts a background session. On end, the host runs its native compiler or test runner.
The host extracts the session ID from the status filename, saves compiler output to a project file, then publishes ID_SESSION-build-result.txt asking the AI to read and address it. Limit retries in your host script to prevent an endless feedback loop.
A team sets beforeCommands to ["/plan"] and afterCommands to ["/act", "/git status"]. Input files contain the planning request. The CLI produces a plan, approves and executes it, then reports Git status.
With hooks enabled, manually submitted prompts emit the same status files. Your desktop integration can show progress or notify another local application without creating input files.
Different sessions can execute concurrently. Requests for the same session wait for its active local cycle and are then executed in order. Interactive plan approvals, questions, and credential unlocking retain their normal CLI behavior.
Create or replace .visidog/hooks/config.json. Creation, modification, rename into place, and deletion are observed. The CLI caches the parsed configuration; it does not read it for each request or inference. Missing keys use defaults and arrays replace their defaults. Deletion restores defaults. Invalid JSON, unknown keys, or invalid values preserve the last valid configuration and display a diagnostic. The configuration file is limited to 64 KiB.
{
"settleMs": 500,
"maxInputBytes": 1048576,
"processModifiedFiles": false,
"beforeCommands": ["/plan"],
"afterCommands": ["/act", "/git status"],
"allowedCommands": [
"/act", "/batch", "/budget", "/cat", "/config", "/context",
"/diff", "/eval", "/exec", "/files", "/git", "/goal", "/help",
"/infinite", "/learn", "/mcp", "/memory", "/model", "/models",
"/obs", "/permissions", "/plan", "/post", "/preview", "/rag",
"/repomap", "/rewind", "/search", "/session-id", "/skills",
"/sql", "/swarm", "/test", "/tts", "/undo", "/voice"
],
"ignoredExtensions": [".tmp", ".part", ".swp"],
"statusEvents": ["start", "run", "end"],
"timestampZone": "utc"
}
| Key | Default | Contract |
|---|---|---|
settleMs | 500 | Event debounce delay, integer 50–10000 ms. This is a one-shot timer, not a scanning interval. |
maxInputBytes | 1048576 | Maximum file size before decoding; integer 1–10485760 bytes. |
processModifiedFiles | false | Normally each filename is admitted once. With true, changed content is admitted again; identical content never replays. |
beforeCommands | [] | Slash commands executed before the file's leading commands. |
afterCommands | [] | Slash commands executed after the file's trailing commands. |
allowedCommands | null | Optional allowed canonical command names, including / and excluding arguments. An empty array disables slash commands. Fixed scope restrictions still apply. |
ignoredExtensions | [".tmp", ".part", ".swp"] | Case-insensitive ignored extensions; use an empty array to remove this filter. |
statusEvents | ["start", "run", "end"] | Transitions to publish; ["end"] suits a completion-only host watcher. |
timestampZone | "local" | Readable timestamp uses "local" or "utc"; the Unix timestamp is unaffected. |
The example includes every canonical command name currently usable in hooks, so you can copy it and remove the commands you do not want to allow. Aliases are checked against their canonical names. Some commands require configured integrations, permissions, or an interactive browser feature. /goal pause and /goal resume require UI action handling and are unsupported in hooks, as are /graph and /stats. Use /help COMMAND for each command's arguments.
With these project defaults, omit the corresponding slash lines from individual input files to avoid executing them twice. Arrays have a maximum of 100 entries. Command entries contain a single slash command line. Configuration is captured at admission, so changing it does not alter commands already queued. Configuration cannot enable hooks: the user's project toggle controls activation.
/swarm graph is rejected like /goal pause, /goal resume, /graph and /stats: those variants require UI rendering that hooks do not provide.
Run the following code in a PowerShell console, or save it as a .ps1 script. Write to an ignored temporary extension in the input directory, close it, then rename it to a unique eligible filename. This keeps partial content out of the admission path.
$project = 'C:\Projects\MyProject'
$inputDir = Join-Path $project '.visidog\hooks\input'
[IO.Directory]::CreateDirectory($inputDir) | Out-Null
$requestId = [Guid]::NewGuid().ToString('N')
$temporary = Join-Path $inputDir ($requestId + '.tmp')
$destination = Join-Path $inputDir ($requestId + '.txt')
$prompt = @'
/plan
Plan the implementation of the acceptance criteria in requirements.md, including verification.
/act
/git status
'@
[IO.File]::WriteAllText($temporary, $prompt, [Text.UTF8Encoding]::new($false))
Move-Item -LiteralPath $temporary -Destination $destination
# Continue a specific session instead:
# $destination = Join-Path $inputDir ($sessionId + '-' + $requestId + '.txt')
Save as watcher.ps1. This loop blocks on events and never scans the directory periodically. It handles Created, Changed and Renamed notifications. It deduplicates repeated notifications by the full snapshot content.
param(
[Parameter(Mandatory = $true)][string]$Project,
[string]$HostExe = 'C:\Tools\OnVisiDogEnd.exe'
)
$eventsDir = Join-Path $Project '.visidog\hooks\events'
[IO.Directory]::CreateDirectory($eventsDir) | Out-Null
$watcher = [IO.FileSystemWatcher]::new($eventsDir, '*-status.txt')
$watcher.IncludeSubdirectories = $false
$watcher.NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite, Size'
$prefix = 'VisiDog.' + [Guid]::NewGuid().ToString('N') + '.'
$sources = @('Created', 'Changed', 'Renamed', 'Error')
$seen = @{}
foreach ($name in $sources) {
Register-ObjectEvent $watcher $name -SourceIdentifier ($prefix + $name) | Out-Null
}
$watcher.EnableRaisingEvents = $true
try {
Write-Host 'Watching VisiDog events. Press Ctrl+C to stop.'
while ($true) {
$event = Wait-Event
if (-not $event.SourceIdentifier.StartsWith($prefix)) { continue }
Remove-Event -EventIdentifier $event.EventIdentifier
if ($event.SourceIdentifier -eq ($prefix + 'Error')) {
throw $event.SourceEventArgs.GetException()
}
$file = $event.SourceEventArgs.FullPath
try { $status = [IO.File]::ReadAllText($file).Trim() }
catch [IO.IOException] { continue } # A subsequent write notification may follow.
if ($status -notmatch '^\d+ - \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} - (start|run|end)$') { continue }
if ($seen[$file] -eq $status) { continue }
$seen[$file] = $status
Write-Host ([IO.Path]::GetFileName($file) + ': ' + $status)
if ($status.EndsWith(' - end')) {
Start-Process -FilePath $HostExe -ArgumentList ('"{0}"' -f $file) -WindowStyle Hidden
}
}
} finally {
$watcher.EnableRaisingEvents = $false
foreach ($name in $sources) { Unregister-Event -SourceIdentifier ($prefix + $name) }
Get-Event | Where-Object { $_.SourceIdentifier.StartsWith($prefix) } | Remove-Event
$watcher.Dispose()
}
Launch it in the background from PowerShell (adjust the paths):
Start-Process -FilePath powershell.exe -WindowStyle Hidden -ArgumentList @(
'-NoProfile', '-File', '"C:\Tools\watcher.ps1"',
'-Project', '"C:\Projects\MyProject"',
'-HostExe', '"C:\Tools\OnVisiDogEnd.exe"'
)
Use an execution policy compatible with your organization's rules. Your host executable receives the full status-file path and can derive the project directory and session ID. Watcher buffer overflow is reported through the Error event; restart and explicitly reconcile outstanding receipts if it occurs.
project="$HOME/projects/my-project"
input_dir="$project/.visidog/hooks/input"
mkdir -p -- "$input_dir"
temporary=$(mktemp "$input_dir/request.XXXXXXXX.tmp")
cat > "$temporary" <<'PROMPT'
/plan
Plan the implementation of the acceptance criteria in requirements.md, including verification.
/act
/git status
PROMPT
mv -- "$temporary" "${temporary%.tmp}.txt"
# To resume, rename to "$input_dir/$session_id-$unique_request.txt" instead.
Install your distribution's inotify-tools package. Save this Bash script as watcher.sh, make it executable, and pass the project directory and host executable. inotifywait -m streams kernel events; it does not poll.
#!/usr/bin/env bash
set -euo pipefail
project=${1:?Usage: watcher.sh PROJECT HOST_EXECUTABLE}
host_exe=${2:?Usage: watcher.sh PROJECT HOST_EXECUTABLE}
events_dir="$project/.visidog/hooks/events"
mkdir -p -- "$events_dir"
declare -A seen=()
while IFS= read -r name; do
[[ "$name" =~ ^[A-Za-z0-9_-]+-status\.txt$ ]] || continue
file="$events_dir/$name"
[[ -f "$file" ]] || continue
status=$(cat -- "$file") || continue
[[ "$status" =~ ^[0-9]+\ -\ [0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2}\ -\ (start|run|end)$ ]] || continue
[[ "${seen[$name]-}" != "$status" ]] || continue
seen[$name]=$status
printf '%s: %s\n' "$name" "$status"
if [[ "$status" == *' - end' ]]; then
"$host_exe" "$file" &
fi
done < <(inotifywait -m -q -e close_write,moved_to --format '%f' -- "$events_dir")
chmod +x ./watcher.sh
nohup ./watcher.sh "$HOME/projects/my-project" /usr/local/bin/on-visidog-end \
> "$HOME/visidog-watcher.log" 2>&1 &
For a persistent service, run the watcher through your usual user service manager. The script listens for closed writes and renames into the events directory, including browser writes that replace a file.
receipts/SHA256_FILENAME.json records admission before execution. The filename hash is SHA-256 over the input filename; hash inside the receipt is SHA-256 over the decoded file contents.
{
"requestId": "a45f8361-13fa-4f21-bb1d-5dc438271004",
"filename": "request.txt",
"hash": "…SHA-256 of the content…",
"sessionId": "session_12345678-1234-1234-abcd-123456789abc",
"state": "done",
"acceptedAt": 1791203600000,
"finishedAt": 1791203696000
}
The receipt progresses from accepted to done or failed. Its unique requestId identifies this admission. Failed receipts include error.code and error.message. Timestamps in receipts are milliseconds. With modified-file processing enabled, the receipt describes the latest admitted version; an earlier completion cannot overwrite it. done means the hook sequence completed; individual slash commands can still report semantic failures in their normal textual result.
accepted receipt. Such a request is not automatically replayed because some commands may already have executed. Inspect the session and submit a new, explicit continuation if needed.Common diagnostics: HOOK_OBSERVER_UNAVAILABLE, HOOK_OBSERVER_FAILED, HOOK_LOCKS_UNAVAILABLE, HOOK_FSA_REQUIRED, HOOK_CONFIG_INVALID, HOOK_INPUT_TOO_LARGE, HOOK_SESSION_ID_INVALID, HOOK_SESSION_PROJECT_MISMATCH, HOOK_SESSION_BUSY, HOOK_WORKSPACE_CHANGED, HOOK_COMMAND_FORBIDDEN, HOOK_COMMAND_UNSUPPORTED, HOOK_PROMPT_REFUSED, HOOK_INFERENCE_FAILED, HOOK_CANCELLED, HOOK_EXECUTION_FAILED. Host command errors use the HOST_COMMAND_* family (UNAVAILABLE, INVALID, TIMEOUT, CANCELLED, WRITE_FAILED, READ_FAILED, RECEIPT_INVALID) plus HOST_COMMAND_SHELL_REQUIRED.
Platform references: Chrome File System Observer API, Microsoft FileSystemWatcher, and inotify-tools.