File Hooks

Connect a local VisiDog CLI project to your host tools using files and native filesystem events.

Enable the integration

  1. Open VisiDog CLI, choose a local folder using File System Access (FSA), and configure an AI model.
  2. Click the hook icon on the folder row, beside the folder picker and OPFS switch. Its highlighted state means hooks are active. Click again to disable them.
  3. Keep the CLI page open. Enabled projects remain monitored when you display another project; the preference is saved per project.
  4. Run a host watcher and publish UTF-8 prompt files into the project's input folder.

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.

Host publishes a promptFSA input eventSession + AI cycleStatus fileHost tool
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

AI-initiated native commands

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.

Input and session protocol

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.

Output status

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
StatusMeaning
startThe first AI inference of a cycle is about to be dispatched.
runAnother AI inference is dispatched in the same cycle, for example after tool results.
endThe 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.

Functional scenarios

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.

Return build results to the same conversation

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.

Plan, then implement

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.

Watch human-driven work

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.

Project-wide config.json

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"
}
KeyDefaultContract
settleMs500Event debounce delay, integer 50–10000 ms. This is a one-shot timer, not a scanning interval.
maxInputBytes1048576Maximum file size before decoding; integer 1–10485760 bytes.
processModifiedFilesfalseNormally 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.
allowedCommandsnullOptional 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.

Windows: publish a request with PowerShell

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')

Watch status events with PowerShell

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.

Linux: publish a request

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.

Watch with inotifywait

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, lifecycle and reliability

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.

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.