Recently our team began testing macOS Ventura (13.x) in our production testing environment. One of the first issues we noticed was that macOS no longer was going to run our loginhooks, either log in or log out...we knew it was coming.
Shell scripts have the ability to trap the exit of a script and perform additional things. We can exploit that to make a Login and Logout script with the same file.
https://community.jamf.com/t5/jamf-pro/how-can-i-use-launchd-to-run-logout-script/td-p/42209
So, let's launch the scripts using /Library/LaunchAgents/org.example.loginhook.plist pointing to the scripts? Sure, that'll work...but no root access for anything we wanted to do "special" for the user. Examples of such might be mounting a shadow disk image to /Applications/, setting display or system sleep times, rebooting on specific conditions, etc...
Ok, how about we add a /Library/LaunchDaemon/org.example.login.plist that launches a signed app, which launches a script to run scripts with admin rights. (Am I the baddie?) We also add a PathState to watch for a /tmp/org.example.login file. When that's created, it runs the script. In this case, we wrap the script in an app.
Platypus.app provides a quick method to wrap a script and codesign it for deployment:
sudo codesign -f -s "Developer ID" -v orgLoginHook.app
Once the app is created and signed, it's placed in /Library/Example/Hools/orgLoginHook.app. This works, but the user can't then stop the script at logout...it just goes on and on. I can trigger the LaunchDaemon by writing a file in /tmp/ (PathState), but it won't stop if I delete the files. In fact, the PathState just gets triggered again when it's deleted. Luckily, it's still running...
How does a standard user stop a LaunchDaemon?
sudo launchctl stop org.example.login
It's possible to allow the standard user to use sudo in the context. (This can't be the best way?) Add the following line to a new file named /etc/sudoers.d/org:
ALL ALL=NOPASSWD: /bin/launchctl stop org.example.login
Users can now stop the script running and trigger the logout events. Logout Hooks are restored.
Comment and let me know what I'm doing wrong or how you also like to abuse your operating system.
Thanks, rusty.
::Files::
/Library/LaunchDaemons/org.example.login.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>org.example.login</string>
<key>PathState</key>
<dict>
<key>/tmp/org.example.login</key>
<true/>
</dict>
<key>RunAtLoad</key>
<false/>
<key>ProgramArguments</key>
<array>
<string>/Library/Example/Hooks/orgLoginHook.app/Contents/MacOS/orgLoginHook</string>
</array>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>org.example.loginhook</string><key>ProgramArguments</key><array><string>/Library/Example/Hooks/orgLogin.sh</string></array><key>RunAtLoad</key><true/></dict></plist>
#! /bin/zsh#3.0# runs everything in /Library/Example/Hooks beginning with LI (for LogIn), LO (for Logout)#--------------------------------------------------------------------------------------------------#-- Log - Echo messages with date and timestamp#--------------------------------------------------------------------------------------------------Log () {logText=$1# indent lines except for program entry and exitif [[ "${logText}" == "-->"* ]];thenechologText="${logText}`basename $0`: launched..."elseif [[ "${logText}" == "<--"* ]];thenlogText="${logText}`basename $0`: ...terminated"elselogText=" ${logText}"fifidate=$(/bin/date)echo "${date/E[DS]T /} ${logText}"}loginmain () {Log "-->"if [[ -d ${HOOKSDIR} ]]; thenfor hook in ${HOOKSDIR}/LI*; doif [[ -s ${hook} && -x ${hook} ]]; thenLog "Executing ${hook} for $loggedInUserID..."# run the item${hook} "$loggedInUser"if [[ $? -ne 0 ]]; thenLog "$0 ${hook} failed!"fifidonefiLog "<--" >> /Library/Example/org/login.log}logoutmain () {Log "-->"if [[ -d ${HOOKSDIR} ]]; thenfor hook in ${HOOKSDIR}/LO*; doif [[ -s ${hook} && -x ${hook} ]]; thenLog "$0 Executing ${hook} for $loggedInUser... "# run the item${hook} "$loggedInUser"if [[ $? -ne 0 ]]; thenLog "$0 ${hook} failed!"fifidonefirm /tmp/org.example.loginLog "<--"}export PATH=/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbinHOOKSDIR="/Library/Example/Hooks"loggedInUser=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }' )onLogout() {echo 'Logging out' >> /Library/Example/org/login.loglogoutmain $loggedInUser 2>&1 >> /Library/Example/org/login.logexit}loginmain $loggedInUser 2>&1 >> /Library/Example/org/login.logtrap 'onLogout' SIGINT SIGHUP SIGTERMwhile true; dosleep 21600 &wait $!done
#! /bin/zsh#3.0#--------------------------------------------------------------------------------------------------#-- Log - Echo messages with date and timestamp#--------------------------------------------------------------------------------------------------Log (){logText=$1# indent lines except for program entry and exitif [[ "${logText}" == "-->"* ]];thenlogText="${logText}`basename $0`: launched..."elseif [[ "${logText}" == "<--"* ]];thenlogText="${logText}`basename $0`: ...terminated"elselogText=" ${logText}"fifidate=$(/bin/date)echo "${date/E[DS]T /} ${logText}"}loginmain () {Log "-->"touch /tmp/org.example.loginLog "<--"}logoutmain () {Log "-->"rm /tmp/org.example.loginsudo launchctl stop org.example.loginLog "<--"}onLogout() {echo 'Logging out' >> /Library/Example/org/login.loglogoutmain $loggedInUser 2>&1 >> /Library/Example/org/login.logexit}export PATH=/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbinloggedInUser=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }' )loginmain $loggedInUser 2>&1 >> /Library/Example/org/login.logtrap 'onLogout' SIGINT SIGHUP SIGTERMwhile true; dosleep 21600 &wait $!done