Two ways in, and three that do not count
You add a directory and want setup to run. Only two paths trigger this event. DirectoryAdded fires after /add-dir, with source: "slash_command", and after an SDK control request named register_repo_root, with source: "register_repo_root". The matcher filters on those two values, and the payload carries directory, the absolute path added, and source.
The hooks reference names three ways a directory can be added that do not fire it: the --add-dir startup flag, the Workspace tab under /permissions, and a failed attempt to add a directory the session already has.
It runs in the background, after the fact
The directory is already live when your hook starts. Sandbox and permission state for the new directory are refreshed and active before the hook runs. Claude Code does not wait for it, and it runs under the 600-second default timeout. Exit 0 can do background work and cannot change the add. Exit 1 cannot undo it. Exit 2 cannot block it, and its stderr goes to the debug log.
Where a message lands depends on source
For slash_command, systemMessage becomes context Claude sees on the next turn, and a failing hook contributes a failure count to the transcript with the full output in debug. For register_repo_root, systemMessage goes to the debug log, and so does failure output. continue is discarded on both, and there is no decision field.
You expect the hook to fire for the directories you pass with --add-dir at startup. It never does.
Start with claude --add-dir ../shared and no hook runs. Type /add-dir ../shared inside the session and it does. The startup flag is one of the three paths that do not fire, and there is no error, because nothing was configured wrong.
For a directory present from startup, the event does not exist. SessionStart is the startup moment.